openrtc 0.1.0
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/README.md +106 -0
- package/dist/OpenRTC.d.ts +71 -0
- package/dist/OpenRTC.js +130 -0
- package/dist/api/MediaTransport.d.ts +7 -0
- package/dist/api/MediaTransport.js +262 -0
- package/dist/api/PlutoPeerConnection.d.ts +50 -0
- package/dist/api/PlutoPeerConnection.js +434 -0
- package/dist/api/PlutoWebSocket.d.ts +24 -0
- package/dist/api/PlutoWebSocket.js +113 -0
- package/dist/api/PlutoWebTransport.d.ts +29 -0
- package/dist/api/PlutoWebTransport.js +105 -0
- package/dist/auth/host.d.ts +73 -0
- package/dist/auth/host.js +513 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/internal.d.ts +1 -0
- package/dist/auth/internal.js +1 -0
- package/dist/compat/index.d.ts +20 -0
- package/dist/compat/index.js +20 -0
- package/dist/core/Client.d.ts +352 -0
- package/dist/core/Client.js +3689 -0
- package/dist/core/Connection.d.ts +67 -0
- package/dist/core/Connection.js +404 -0
- package/dist/core/IEngineBridge.d.ts +18 -0
- package/dist/core/IEngineBridge.js +1 -0
- package/dist/core/ISignalingTransport.d.ts +13 -0
- package/dist/core/ISignalingTransport.js +1 -0
- package/dist/core/PeerLifecycleManager.d.ts +43 -0
- package/dist/core/PeerLifecycleManager.js +330 -0
- package/dist/core/PeerProjectionStore.d.ts +43 -0
- package/dist/core/PeerProjectionStore.js +327 -0
- package/dist/core/PlutoRTC.d.ts +13 -0
- package/dist/core/PlutoRTC.js +26 -0
- package/dist/core/Room.d.ts +47 -0
- package/dist/core/Room.js +376 -0
- package/dist/core/Signaling.d.ts +39 -0
- package/dist/core/Signaling.js +147 -0
- package/dist/core/bridges/BridgeCodecs.d.ts +19 -0
- package/dist/core/bridges/BridgeCodecs.js +317 -0
- package/dist/core/bridges/BrowserFirestoreRooms.d.ts +36 -0
- package/dist/core/bridges/BrowserFirestoreRooms.js +169 -0
- package/dist/core/bridges/BrowserFirestoreSignaling.d.ts +111 -0
- package/dist/core/bridges/BrowserFirestoreSignaling.js +1010 -0
- package/dist/core/bridges/NativeIpcBridge.d.ts +156 -0
- package/dist/core/bridges/NativeIpcBridge.js +1472 -0
- package/dist/core/bridges/RtdbPresence.d.ts +122 -0
- package/dist/core/bridges/RtdbPresence.js +201 -0
- package/dist/core/bridges/TauriBridge.d.ts +8 -0
- package/dist/core/bridges/TauriBridge.js +7 -0
- package/dist/core/bridges/WasmBridge.d.ts +233 -0
- package/dist/core/bridges/WasmBridge.js +1811 -0
- package/dist/core/config.d.ts +31 -0
- package/dist/core/config.js +138 -0
- package/dist/core/transports/ITransport.d.ts +12 -0
- package/dist/core/transports/ITransport.js +1 -0
- package/dist/core/transports/LocalDaemonBridge.d.ts +41 -0
- package/dist/core/transports/LocalDaemonBridge.js +227 -0
- package/dist/core/transports/TauriSignaling.d.ts +28 -0
- package/dist/core/transports/TauriSignaling.js +373 -0
- package/dist/core/transports/TransportFactory.d.ts +9 -0
- package/dist/core/transports/TransportFactory.js +59 -0
- package/dist/core/transports/WasmSignaling.d.ts +78 -0
- package/dist/core/transports/WasmSignaling.js +522 -0
- package/dist/core/transports/WebRTCTransport.d.ts +23 -0
- package/dist/core/transports/WebRTCTransport.js +132 -0
- package/dist/core/trust.d.ts +23 -0
- package/dist/core/trust.js +99 -0
- package/dist/core/types.d.ts +559 -0
- package/dist/core/types.js +19 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/moq/MoQMediaTransport.d.ts +6 -0
- package/dist/moq/MoQMediaTransport.js +106 -0
- package/dist/moq/MoQPublisher.d.ts +22 -0
- package/dist/moq/MoQPublisher.js +179 -0
- package/dist/moq/MoQSession.d.ts +35 -0
- package/dist/moq/MoQSession.js +166 -0
- package/dist/moq/MoQSubscriber.d.ts +19 -0
- package/dist/moq/MoQSubscriber.js +235 -0
- package/dist/moq/common.d.ts +47 -0
- package/dist/moq/common.js +304 -0
- package/dist/moq/messages.d.ts +105 -0
- package/dist/moq/messages.js +367 -0
- package/dist/presets/plutoHosted.d.ts +13 -0
- package/dist/presets/plutoHosted.js +18 -0
- package/dist/runtime/DelegatingRuntimeAdapter.d.ts +68 -0
- package/dist/runtime/DelegatingRuntimeAdapter.js +250 -0
- package/dist/runtime/IpcRuntimeAdapter.d.ts +9 -0
- package/dist/runtime/IpcRuntimeAdapter.js +13 -0
- package/dist/runtime/RuntimeClient.d.ts +257 -0
- package/dist/runtime/RuntimeClient.js +1484 -0
- package/dist/runtime/TauriRuntimeAdapter.d.ts +1 -0
- package/dist/runtime/TauriRuntimeAdapter.js +1 -0
- package/dist/runtime/WasmRuntimeAdapter.d.ts +6 -0
- package/dist/runtime/WasmRuntimeAdapter.js +11 -0
- package/dist/runtime/factories.d.ts +24 -0
- package/dist/runtime/factories.js +66 -0
- package/dist/runtime/index.d.ts +6 -0
- package/dist/runtime/index.js +5 -0
- package/dist/runtime/ipc.d.ts +8 -0
- package/dist/runtime/ipc.js +65 -0
- package/dist/runtime/ipcContract.d.ts +46 -0
- package/dist/runtime/ipcContract.js +1 -0
- package/dist/runtime/tauri.d.ts +16 -0
- package/dist/runtime/tauri.js +22 -0
- package/dist/runtime/types.d.ts +110 -0
- package/dist/runtime/types.js +1 -0
- package/dist/transports/TauriTransport.d.ts +17 -0
- package/dist/transports/TauriTransport.js +49 -0
- package/dist/transports/WebTransport.d.ts +23 -0
- package/dist/transports/WebTransport.js +71 -0
- package/dist/utils/debug.d.ts +4 -0
- package/dist/utils/debug.js +33 -0
- package/dist/utils/runtime.d.ts +1 -0
- package/dist/utils/runtime.js +10 -0
- package/package.json +97 -0
- package/wasm/README.md +64 -0
- package/wasm/openrtc.d.ts +284 -0
- package/wasm/openrtc.js +2132 -0
- package/wasm/openrtc_bg.wasm +0 -0
- package/wasm/openrtc_bg.wasm.d.ts +110 -0
- package/wasm/package.json +16 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
function coerceRecord(value) {
|
|
2
|
+
if (!value || typeof value !== 'object') {
|
|
3
|
+
return {};
|
|
4
|
+
}
|
|
5
|
+
if (value instanceof Map) {
|
|
6
|
+
return Object.fromEntries(value);
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function flattenRecord(value) {
|
|
11
|
+
const record = coerceRecord(value);
|
|
12
|
+
const nestedDevice = coerceRecord(record.device);
|
|
13
|
+
if (!Object.keys(nestedDevice).length) {
|
|
14
|
+
return record;
|
|
15
|
+
}
|
|
16
|
+
const flattened = { ...nestedDevice, ...record };
|
|
17
|
+
delete flattened.device;
|
|
18
|
+
return flattened;
|
|
19
|
+
}
|
|
20
|
+
export function pickString(raw, ...keys) {
|
|
21
|
+
for (const key of keys) {
|
|
22
|
+
const value = raw[key];
|
|
23
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
export function pickBoolean(raw, ...keys) {
|
|
30
|
+
for (const key of keys) {
|
|
31
|
+
const value = raw[key];
|
|
32
|
+
if (typeof value === 'boolean') {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
export function pickNumber(raw, ...keys) {
|
|
39
|
+
for (const key of keys) {
|
|
40
|
+
const value = raw[key];
|
|
41
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
export function isPersonalSpaceMode(options) {
|
|
48
|
+
return (options.discoveryMode ?? 'personal') === 'personal'
|
|
49
|
+
&& typeof options.apiKey === 'string'
|
|
50
|
+
&& options.apiKey.trim().length > 0
|
|
51
|
+
&& typeof options.spaceKey === 'string'
|
|
52
|
+
&& options.spaceKey.trim().length > 0;
|
|
53
|
+
}
|
|
54
|
+
export function matchesDiscoveryTag(device, appTag, personalMode) {
|
|
55
|
+
if (personalMode) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
const deviceAppTag = typeof device.appTag === 'string'
|
|
59
|
+
? device.appTag
|
|
60
|
+
: typeof device.tag === 'string'
|
|
61
|
+
? device.tag
|
|
62
|
+
: undefined;
|
|
63
|
+
return deviceAppTag === appTag;
|
|
64
|
+
}
|
|
65
|
+
export function normalizeDeviceRecord(raw, fallbackId) {
|
|
66
|
+
const normalizedRaw = flattenRecord(raw);
|
|
67
|
+
const deviceId = pickString(normalizedRaw, 'deviceId', 'device_id') || (fallbackId || '');
|
|
68
|
+
const nodeId = pickString(normalizedRaw, 'nodeId', 'node_id') || '';
|
|
69
|
+
const ticket = pickString(normalizedRaw, 'ticket') || '';
|
|
70
|
+
return {
|
|
71
|
+
appTag: pickString(normalizedRaw, 'appTag', 'app_tag', 'tag'),
|
|
72
|
+
deviceId,
|
|
73
|
+
deviceName: pickString(normalizedRaw, 'deviceName', 'device_name') || 'Unknown Device',
|
|
74
|
+
online: !!normalizedRaw.online,
|
|
75
|
+
ticket,
|
|
76
|
+
tag: pickString(normalizedRaw, 'tag', 'appTag', 'app_tag'),
|
|
77
|
+
kind: pickString(normalizedRaw, 'kind'),
|
|
78
|
+
nodeId: nodeId || undefined,
|
|
79
|
+
platformType: pickString(normalizedRaw, 'platformType', 'platform_type'),
|
|
80
|
+
capabilities: normalizedRaw.capabilities || undefined,
|
|
81
|
+
sessionId: pickString(normalizedRaw, 'sessionId', 'session_id'),
|
|
82
|
+
userId: pickString(normalizedRaw, 'userId', 'user_id'),
|
|
83
|
+
lastSeenAt: normalizedRaw.lastSeenAt ?? normalizedRaw.last_seen_at,
|
|
84
|
+
expiresAt: normalizedRaw.expiresAt ?? normalizedRaw.expires_at,
|
|
85
|
+
createdAt: normalizedRaw.createdAt ?? normalizedRaw.created_at,
|
|
86
|
+
updatedAt: normalizedRaw.updatedAt ?? normalizedRaw.updated_at,
|
|
87
|
+
metadata: pickString(normalizedRaw, 'metadata'),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function normalizeDeviceStatusSnapshotRecord(raw) {
|
|
91
|
+
const normalizedRaw = flattenRecord(raw);
|
|
92
|
+
return {
|
|
93
|
+
...normalizeDeviceRecord(normalizedRaw),
|
|
94
|
+
connectionStatus: pickString(normalizedRaw, 'connectionStatus', 'connection_status') ||
|
|
95
|
+
'disconnected',
|
|
96
|
+
settledReady: pickBoolean(normalizedRaw, 'settledReady', 'settled_ready') ?? false,
|
|
97
|
+
readinessState: pickString(normalizedRaw, 'readinessState', 'readiness_state') ?? undefined,
|
|
98
|
+
readinessReason: pickString(normalizedRaw, 'readinessReason', 'readiness_reason') ?? undefined,
|
|
99
|
+
peerHealth: pickString(normalizedRaw, 'peerHealth', 'peer_health') ||
|
|
100
|
+
'unknown',
|
|
101
|
+
peerId: pickString(normalizedRaw, 'peerId', 'peer_id'),
|
|
102
|
+
scopes: Array.isArray(normalizedRaw.scopes)
|
|
103
|
+
? normalizedRaw.scopes.filter((scope) => typeof scope === 'string')
|
|
104
|
+
: [],
|
|
105
|
+
connectionId: pickString(normalizedRaw, 'connectionId', 'connection_id'),
|
|
106
|
+
deviceIdHint: pickString(normalizedRaw, 'deviceIdHint', 'device_id_hint'),
|
|
107
|
+
activeTransportStableId: pickNumber(normalizedRaw, 'activeTransportStableId', 'active_transport_stable_id') ?? undefined,
|
|
108
|
+
transportGeneration: pickNumber(normalizedRaw, 'transportGeneration', 'transport_generation') ?? undefined,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function normalizePeerSessionSnapshotRecord(raw) {
|
|
112
|
+
const normalizedRaw = flattenRecord(raw);
|
|
113
|
+
return {
|
|
114
|
+
peerId: pickString(normalizedRaw, 'peerId', 'peer_id') || '',
|
|
115
|
+
deviceId: pickString(normalizedRaw, 'deviceId', 'device_id'),
|
|
116
|
+
deviceIdHint: pickString(normalizedRaw, 'deviceIdHint', 'device_id_hint'),
|
|
117
|
+
nodeId: pickString(normalizedRaw, 'nodeId', 'node_id'),
|
|
118
|
+
activeConnectionId: pickString(normalizedRaw, 'activeConnectionId', 'active_connection_id'),
|
|
119
|
+
candidateConnectionIds: Array.isArray(normalizedRaw.candidateConnectionIds)
|
|
120
|
+
? normalizedRaw.candidateConnectionIds.filter((value) => typeof value === 'string')
|
|
121
|
+
: Array.isArray(normalizedRaw.candidate_connection_ids)
|
|
122
|
+
? normalizedRaw.candidate_connection_ids.filter((value) => typeof value === 'string')
|
|
123
|
+
: [],
|
|
124
|
+
status: pickString(normalizedRaw, 'status') ||
|
|
125
|
+
'disconnected',
|
|
126
|
+
health: pickString(normalizedRaw, 'health') ||
|
|
127
|
+
'unknown',
|
|
128
|
+
settledReady: pickBoolean(normalizedRaw, 'settledReady', 'settled_ready') ?? false,
|
|
129
|
+
readinessState: pickString(normalizedRaw, 'readinessState', 'readiness_state') ?? undefined,
|
|
130
|
+
activeTransportStableId: pickNumber(normalizedRaw, 'activeTransportStableId', 'active_transport_stable_id') ?? undefined,
|
|
131
|
+
transportGeneration: pickNumber(normalizedRaw, 'transportGeneration', 'transport_generation') ?? undefined,
|
|
132
|
+
replacementPending: pickBoolean(normalizedRaw, 'replacementPending', 'replacement_pending') ?? undefined,
|
|
133
|
+
lastLifecycleTransitionAtMs: pickNumber(normalizedRaw, 'lastLifecycleTransitionAtMs', 'last_lifecycle_transition_at_ms') ?? undefined,
|
|
134
|
+
readinessReason: pickString(normalizedRaw, 'readinessReason', 'readiness_reason') ?? undefined,
|
|
135
|
+
transitionCount: pickNumber(normalizedRaw, 'transitionCount', 'transition_count') ?? undefined,
|
|
136
|
+
connectingTransitionCount: pickNumber(normalizedRaw, 'connectingTransitionCount', 'connecting_transition_count') ?? undefined,
|
|
137
|
+
replacementCount: pickNumber(normalizedRaw, 'replacementCount', 'replacement_count') ?? undefined,
|
|
138
|
+
retireCount: pickNumber(normalizedRaw, 'retireCount', 'retire_count') ?? undefined,
|
|
139
|
+
lastDisconnectReason: pickString(normalizedRaw, 'lastDisconnectReason', 'last_disconnect_reason') ?? undefined,
|
|
140
|
+
lastReconnectReason: pickString(normalizedRaw, 'lastReconnectReason', 'last_reconnect_reason') ?? undefined,
|
|
141
|
+
scopes: Array.isArray(normalizedRaw.scopes)
|
|
142
|
+
? normalizedRaw.scopes.filter((scope) => typeof scope === 'string')
|
|
143
|
+
: [],
|
|
144
|
+
lastSeenAtMs: typeof normalizedRaw.lastSeenAtMs === 'number'
|
|
145
|
+
? normalizedRaw.lastSeenAtMs
|
|
146
|
+
: typeof normalizedRaw.last_seen_at_ms === 'number'
|
|
147
|
+
? normalizedRaw.last_seen_at_ms
|
|
148
|
+
: 0,
|
|
149
|
+
error: pickString(normalizedRaw, 'error'),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
export function normalizeManagedConnectionRecord(raw) {
|
|
153
|
+
const record = coerceRecord(raw);
|
|
154
|
+
return {
|
|
155
|
+
connectionId: typeof record.connectionId === 'string' ? record.connectionId : '',
|
|
156
|
+
nodeId: typeof record.nodeId === 'string' ? record.nodeId : null,
|
|
157
|
+
deviceId: typeof record.deviceId === 'string' ? record.deviceId : null,
|
|
158
|
+
deviceIdHint: typeof record.deviceIdHint === 'string' ? record.deviceIdHint : null,
|
|
159
|
+
endpointId: typeof record.endpointId === 'string' ? record.endpointId : null,
|
|
160
|
+
transportGeneration: typeof record.transportGeneration === 'number' ? record.transportGeneration : 0,
|
|
161
|
+
transportStableId: typeof record.transportStableId === 'number' ? record.transportStableId : null,
|
|
162
|
+
transportSource: typeof record.transportSource === 'string' ? record.transportSource : null,
|
|
163
|
+
lastTransportChangeAtMs: typeof record.lastTransportChangeAtMs === 'number'
|
|
164
|
+
? record.lastTransportChangeAtMs
|
|
165
|
+
: 0,
|
|
166
|
+
state: typeof record.state === 'string' ? record.state : 'pending',
|
|
167
|
+
statusReason: typeof record.statusReason === 'string' ? record.statusReason : null,
|
|
168
|
+
transitionCount: typeof record.transitionCount === 'number' ? record.transitionCount : undefined,
|
|
169
|
+
connectingTransitionCount: typeof record.connectingTransitionCount === 'number'
|
|
170
|
+
? record.connectingTransitionCount
|
|
171
|
+
: undefined,
|
|
172
|
+
replacementCount: typeof record.replacementCount === 'number' ? record.replacementCount : undefined,
|
|
173
|
+
retireCount: typeof record.retireCount === 'number' ? record.retireCount : undefined,
|
|
174
|
+
lastDisconnectReason: typeof record.lastDisconnectReason === 'string'
|
|
175
|
+
? record.lastDisconnectReason
|
|
176
|
+
: undefined,
|
|
177
|
+
lastReconnectReason: typeof record.lastReconnectReason === 'string'
|
|
178
|
+
? record.lastReconnectReason
|
|
179
|
+
: undefined,
|
|
180
|
+
createdAtMs: typeof record.createdAtMs === 'number' ? record.createdAtMs : 0,
|
|
181
|
+
updatedAtMs: typeof record.updatedAtMs === 'number' ? record.updatedAtMs : 0,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export function normalizeResolvedPeerIdentity(raw) {
|
|
185
|
+
const record = coerceRecord(raw);
|
|
186
|
+
return {
|
|
187
|
+
peerId: typeof record.peerId === 'string' ? record.peerId : null,
|
|
188
|
+
deviceId: typeof record.deviceId === 'string' ? record.deviceId : null,
|
|
189
|
+
deviceIdHint: typeof record.deviceIdHint === 'string' ? record.deviceIdHint : null,
|
|
190
|
+
nodeId: typeof record.nodeId === 'string' ? record.nodeId : null,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
export function mapPeerSessionStatusToBackendState(status) {
|
|
194
|
+
switch (status) {
|
|
195
|
+
case 'connected':
|
|
196
|
+
return 'connected';
|
|
197
|
+
case 'connecting':
|
|
198
|
+
return 'connecting';
|
|
199
|
+
case 'failed':
|
|
200
|
+
return 'failed';
|
|
201
|
+
case 'closed':
|
|
202
|
+
case 'disconnected':
|
|
203
|
+
default:
|
|
204
|
+
return 'closed';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
export function peerSessionToBackendConnectionState(snapshot) {
|
|
208
|
+
const connectionId = snapshot.activeConnectionId || snapshot.candidateConnectionIds[0] || null;
|
|
209
|
+
if (!connectionId) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const state = mapPeerSessionStatusToBackendState(snapshot.status);
|
|
213
|
+
const transportState = state === 'connected'
|
|
214
|
+
? 'connected'
|
|
215
|
+
: state === 'connecting'
|
|
216
|
+
? 'connecting'
|
|
217
|
+
: 'closed';
|
|
218
|
+
const protocolState = state === 'connected'
|
|
219
|
+
? (snapshot.settledReady ? 'routable' : 'transport-only')
|
|
220
|
+
: state === 'connecting'
|
|
221
|
+
? 'connecting'
|
|
222
|
+
: 'closed';
|
|
223
|
+
return {
|
|
224
|
+
connectionId,
|
|
225
|
+
deviceId: snapshot.deviceId ?? null,
|
|
226
|
+
deviceIdHint: snapshot.deviceIdHint ?? null,
|
|
227
|
+
remoteNodeId: snapshot.nodeId ?? null,
|
|
228
|
+
state,
|
|
229
|
+
transportState,
|
|
230
|
+
protocolState,
|
|
231
|
+
routable: !!snapshot.settledReady,
|
|
232
|
+
readinessState: snapshot.readinessState,
|
|
233
|
+
readinessReason: snapshot.readinessReason,
|
|
234
|
+
transportGeneration: snapshot.transportGeneration,
|
|
235
|
+
activeTransportStableId: snapshot.activeTransportStableId,
|
|
236
|
+
replacementInProgress: snapshot.replacementPending,
|
|
237
|
+
lastLifecycleTransitionAtMs: snapshot.lastLifecycleTransitionAtMs,
|
|
238
|
+
transitionCount: snapshot.transitionCount,
|
|
239
|
+
connectingTransitionCount: snapshot.connectingTransitionCount,
|
|
240
|
+
replacementCount: snapshot.replacementCount,
|
|
241
|
+
retireCount: snapshot.retireCount,
|
|
242
|
+
lastDisconnectReason: snapshot.lastDisconnectReason ?? undefined,
|
|
243
|
+
lastReconnectReason: snapshot.lastReconnectReason ?? undefined,
|
|
244
|
+
updatedAt: snapshot.lastSeenAtMs,
|
|
245
|
+
error: snapshot.error ?? undefined,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
export function normalizeConnectionStateSnapshot(raw) {
|
|
249
|
+
const record = coerceRecord(raw);
|
|
250
|
+
const connectionId = pickString(record, 'connectionId', 'connection_id');
|
|
251
|
+
if (!connectionId) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const state = pickString(record, 'state') || 'connecting';
|
|
255
|
+
const createdAt = pickNumber(record, 'createdAt', 'created_at')
|
|
256
|
+
?? pickNumber(record, 'updatedAt', 'updated_at')
|
|
257
|
+
?? Date.now();
|
|
258
|
+
const updatedAt = pickNumber(record, 'updatedAt', 'updated_at')
|
|
259
|
+
?? createdAt;
|
|
260
|
+
return {
|
|
261
|
+
connectionId,
|
|
262
|
+
deviceId: pickString(record, 'deviceId', 'device_id'),
|
|
263
|
+
deviceIdHint: pickString(record, 'deviceIdHint', 'device_id_hint'),
|
|
264
|
+
remoteNodeId: pickString(record, 'remoteNodeId', 'remote_node_id'),
|
|
265
|
+
state,
|
|
266
|
+
transportState: pickString(record, 'transportState', 'transport_state')
|
|
267
|
+
?? (state === 'connected'
|
|
268
|
+
? 'connected'
|
|
269
|
+
: state === 'connecting'
|
|
270
|
+
? 'connecting'
|
|
271
|
+
: 'closed'),
|
|
272
|
+
protocolState: pickString(record, 'protocolState', 'protocol_state')
|
|
273
|
+
?? (state === 'connected'
|
|
274
|
+
? 'routable'
|
|
275
|
+
: state === 'connecting'
|
|
276
|
+
? 'connecting'
|
|
277
|
+
: 'closed'),
|
|
278
|
+
routable: pickBoolean(record, 'routable') ?? false,
|
|
279
|
+
readinessState: pickString(record, 'readinessState', 'readiness_state') ?? undefined,
|
|
280
|
+
readinessReason: pickString(record, 'readinessReason', 'readiness_reason') ?? undefined,
|
|
281
|
+
transportGeneration: pickNumber(record, 'transportGeneration', 'transport_generation') ?? undefined,
|
|
282
|
+
activeTransportStableId: pickNumber(record, 'activeTransportStableId', 'active_transport_stable_id') ?? undefined,
|
|
283
|
+
replacementInProgress: pickBoolean(record, 'replacementInProgress', 'replacement_in_progress') ?? undefined,
|
|
284
|
+
lastLifecycleTransitionAtMs: pickNumber(record, 'lastLifecycleTransitionAtMs', 'last_lifecycle_transition_at_ms') ?? undefined,
|
|
285
|
+
transitionCount: pickNumber(record, 'transitionCount', 'transition_count') ?? undefined,
|
|
286
|
+
connectingTransitionCount: pickNumber(record, 'connectingTransitionCount', 'connecting_transition_count') ?? undefined,
|
|
287
|
+
replacementCount: pickNumber(record, 'replacementCount', 'replacement_count') ?? undefined,
|
|
288
|
+
retireCount: pickNumber(record, 'retireCount', 'retire_count') ?? undefined,
|
|
289
|
+
lastDisconnectReason: pickString(record, 'lastDisconnectReason', 'last_disconnect_reason') ?? undefined,
|
|
290
|
+
lastReconnectReason: pickString(record, 'lastReconnectReason', 'last_reconnect_reason') ?? undefined,
|
|
291
|
+
error: pickString(record, 'error') ?? undefined,
|
|
292
|
+
createdAt,
|
|
293
|
+
updatedAt,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
export function sameConnectionState(left, right) {
|
|
297
|
+
if (!left) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
return left.connectionId === right.connectionId
|
|
301
|
+
&& (left.deviceId ?? null) === (right.deviceId ?? null)
|
|
302
|
+
&& (left.deviceIdHint ?? null) === (right.deviceIdHint ?? null)
|
|
303
|
+
&& (left.remoteNodeId ?? null) === (right.remoteNodeId ?? null)
|
|
304
|
+
&& left.state === right.state
|
|
305
|
+
&& (left.transportState ?? null) === (right.transportState ?? null)
|
|
306
|
+
&& (left.protocolState ?? null) === (right.protocolState ?? null)
|
|
307
|
+
&& !!left.routable === !!right.routable
|
|
308
|
+
&& (left.transportGeneration ?? null) === (right.transportGeneration ?? null)
|
|
309
|
+
&& !!left.replacementInProgress === !!right.replacementInProgress
|
|
310
|
+
&& (left.transitionCount ?? null) === (right.transitionCount ?? null)
|
|
311
|
+
&& (left.connectingTransitionCount ?? null) === (right.connectingTransitionCount ?? null)
|
|
312
|
+
&& (left.replacementCount ?? null) === (right.replacementCount ?? null)
|
|
313
|
+
&& (left.retireCount ?? null) === (right.retireCount ?? null)
|
|
314
|
+
&& (left.lastDisconnectReason ?? null) === (right.lastDisconnectReason ?? null)
|
|
315
|
+
&& (left.lastReconnectReason ?? null) === (right.lastReconnectReason ?? null)
|
|
316
|
+
&& (left.error ?? null) === (right.error ?? null);
|
|
317
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { FirebaseApp } from 'firebase/app';
|
|
2
|
+
import type { RoomMember } from '../types';
|
|
3
|
+
import { RtdbPresence } from './RtdbPresence';
|
|
4
|
+
export interface BrowserFirestoreRoomsOptions {
|
|
5
|
+
app: FirebaseApp;
|
|
6
|
+
getRoomBackendTag: () => string;
|
|
7
|
+
}
|
|
8
|
+
export declare class BrowserFirestoreRooms {
|
|
9
|
+
private readonly options;
|
|
10
|
+
private readonly firestore;
|
|
11
|
+
private rtdbPresence;
|
|
12
|
+
constructor(options: BrowserFirestoreRoomsOptions);
|
|
13
|
+
/**
|
|
14
|
+
* Wire in the RTDB presence instance. Called by RoomManager after join.
|
|
15
|
+
* Once set, all subsequent watchMembers calls use the RTDB overlay.
|
|
16
|
+
*/
|
|
17
|
+
setRtdbPresence(rtdbPresence: RtdbPresence): void;
|
|
18
|
+
watchMembers(roomId: string, myNodeId: string, callback: (members: RoomMember[]) => void): () => void;
|
|
19
|
+
/**
|
|
20
|
+
* RTDB-backed member watch (Phase 2).
|
|
21
|
+
*
|
|
22
|
+
* Firestore onSnapshot provides the authoritative roster (join/leave only).
|
|
23
|
+
* RTDB onRoomPresenceChange provides the live online/offline boolean.
|
|
24
|
+
* The merged list is re-emitted whenever either source changes.
|
|
25
|
+
*/
|
|
26
|
+
private watchMembersWithRtdb;
|
|
27
|
+
private getNamespaceRoot;
|
|
28
|
+
/**
|
|
29
|
+
* Returns the appTag used for RTDB room presence paths.
|
|
30
|
+
* Personal spaces use "space::{namespaceId}" as the appTag in RTDB too.
|
|
31
|
+
*/
|
|
32
|
+
private getAppTag;
|
|
33
|
+
private toRoomMember;
|
|
34
|
+
private isExpired;
|
|
35
|
+
private toMillis;
|
|
36
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { collection, getFirestore, onSnapshot, } from 'firebase/firestore';
|
|
2
|
+
const ROOM_MEMBER_TTL_MS = 600000;
|
|
3
|
+
export class BrowserFirestoreRooms {
|
|
4
|
+
constructor(options) {
|
|
5
|
+
this.options = options;
|
|
6
|
+
// Phase 2: RTDB presence overlay for room members.
|
|
7
|
+
// When set, watchMembers uses Firestore onSnapshot for the roster (join/leave
|
|
8
|
+
// events only) and RTDB for the live online/offline boolean per member.
|
|
9
|
+
this.rtdbPresence = null;
|
|
10
|
+
this.firestore = getFirestore(options.app);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Wire in the RTDB presence instance. Called by RoomManager after join.
|
|
14
|
+
* Once set, all subsequent watchMembers calls use the RTDB overlay.
|
|
15
|
+
*/
|
|
16
|
+
setRtdbPresence(rtdbPresence) {
|
|
17
|
+
this.rtdbPresence = rtdbPresence;
|
|
18
|
+
}
|
|
19
|
+
watchMembers(roomId, myNodeId, callback) {
|
|
20
|
+
const membersCollection = collection(this.firestore, `${this.getNamespaceRoot()}/rooms/${roomId}/members`);
|
|
21
|
+
// Phase 2: RTDB presence overlay.
|
|
22
|
+
// When RTDB is available, we maintain a live presenceMap alongside the
|
|
23
|
+
// Firestore roster. Any change to either triggers a re-emit of the
|
|
24
|
+
// merged member list. This means Firestore onSnapshot only fires on
|
|
25
|
+
// actual join/leave events (not heartbeats), while the online/offline
|
|
26
|
+
// boolean is driven by RTDB in near-real-time.
|
|
27
|
+
if (this.rtdbPresence) {
|
|
28
|
+
return this.watchMembersWithRtdb(roomId, myNodeId, membersCollection, callback);
|
|
29
|
+
}
|
|
30
|
+
// Fallback: legacy Firestore-only watch (no RTDB).
|
|
31
|
+
return onSnapshot(membersCollection, (snapshot) => {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const members = snapshot.docs
|
|
34
|
+
.map((document) => this.toRoomMember(document))
|
|
35
|
+
.filter((member) => !!member)
|
|
36
|
+
.filter((member) => member.nodeId !== myNodeId)
|
|
37
|
+
.filter((member) => !this.isExpired(member, now))
|
|
38
|
+
.sort((left, right) => {
|
|
39
|
+
if (left.joinedAt !== right.joinedAt) {
|
|
40
|
+
return left.joinedAt - right.joinedAt;
|
|
41
|
+
}
|
|
42
|
+
return left.nodeId.localeCompare(right.nodeId);
|
|
43
|
+
});
|
|
44
|
+
callback(members);
|
|
45
|
+
}, (error) => {
|
|
46
|
+
console.warn('[BrowserFirestoreRooms] room watch failed:', error);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* RTDB-backed member watch (Phase 2).
|
|
51
|
+
*
|
|
52
|
+
* Firestore onSnapshot provides the authoritative roster (join/leave only).
|
|
53
|
+
* RTDB onRoomPresenceChange provides the live online/offline boolean.
|
|
54
|
+
* The merged list is re-emitted whenever either source changes.
|
|
55
|
+
*/
|
|
56
|
+
watchMembersWithRtdb(roomId, myNodeId, membersCollection, callback) {
|
|
57
|
+
const rtdb = this.rtdbPresence;
|
|
58
|
+
const appTag = this.getAppTag();
|
|
59
|
+
let firestoreMembers = [];
|
|
60
|
+
let rtdbPresenceMap = {};
|
|
61
|
+
const emit = () => {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const merged = firestoreMembers
|
|
64
|
+
.filter((m) => m.nodeId !== myNodeId)
|
|
65
|
+
.filter((m) => !this.isExpired(m, now))
|
|
66
|
+
// Hide members RTDB has confirmed offline (crashed without leaving).
|
|
67
|
+
// They stay in the Firestore roster until TTL/cleanup removes them,
|
|
68
|
+
// but we don't show them as connectable peers.
|
|
69
|
+
.filter((m) => {
|
|
70
|
+
const rtdbState = rtdbPresenceMap[m.nodeId];
|
|
71
|
+
// If RTDB has seen this member and says offline → hide.
|
|
72
|
+
if (rtdbState && !rtdbState.online)
|
|
73
|
+
return false;
|
|
74
|
+
return true;
|
|
75
|
+
})
|
|
76
|
+
.sort((left, right) => {
|
|
77
|
+
if (left.joinedAt !== right.joinedAt) {
|
|
78
|
+
return left.joinedAt - right.joinedAt;
|
|
79
|
+
}
|
|
80
|
+
return left.nodeId.localeCompare(right.nodeId);
|
|
81
|
+
});
|
|
82
|
+
callback(merged);
|
|
83
|
+
};
|
|
84
|
+
// Subscribe to Firestore for roster changes (join/leave only after Phase 2).
|
|
85
|
+
const unsubscribeFirestore = onSnapshot(membersCollection, (snapshot) => {
|
|
86
|
+
firestoreMembers = snapshot.docs
|
|
87
|
+
.map((document) => this.toRoomMember(document))
|
|
88
|
+
.filter((member) => !!member);
|
|
89
|
+
emit();
|
|
90
|
+
}, (error) => {
|
|
91
|
+
console.warn('[BrowserFirestoreRooms] room watch failed:', error);
|
|
92
|
+
});
|
|
93
|
+
// Subscribe to RTDB for live liveness.
|
|
94
|
+
const unsubscribeRtdb = rtdb.onRoomPresenceChange(appTag, roomId, (presenceMap) => {
|
|
95
|
+
rtdbPresenceMap = presenceMap;
|
|
96
|
+
emit();
|
|
97
|
+
});
|
|
98
|
+
return () => {
|
|
99
|
+
unsubscribeFirestore();
|
|
100
|
+
unsubscribeRtdb();
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
getNamespaceRoot() {
|
|
104
|
+
const backendTag = this.options.getRoomBackendTag().trim();
|
|
105
|
+
if (!backendTag) {
|
|
106
|
+
throw new Error('Room backend tag is required for browser room watches');
|
|
107
|
+
}
|
|
108
|
+
if (backendTag.startsWith('space::')) {
|
|
109
|
+
return `spaces/${backendTag.slice('space::'.length)}`;
|
|
110
|
+
}
|
|
111
|
+
return `apps/${backendTag}`;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Returns the appTag used for RTDB room presence paths.
|
|
115
|
+
* Personal spaces use "space::{namespaceId}" as the appTag in RTDB too.
|
|
116
|
+
*/
|
|
117
|
+
getAppTag() {
|
|
118
|
+
return this.options.getRoomBackendTag().trim();
|
|
119
|
+
}
|
|
120
|
+
toRoomMember(document) {
|
|
121
|
+
const raw = document.data();
|
|
122
|
+
const nodeId = typeof raw.nodeId === 'string' && raw.nodeId.trim().length > 0
|
|
123
|
+
? raw.nodeId.trim()
|
|
124
|
+
: document.id;
|
|
125
|
+
const ticket = typeof raw.ticket === 'string' ? raw.ticket : '';
|
|
126
|
+
return {
|
|
127
|
+
nodeId,
|
|
128
|
+
userId: typeof raw.userId === 'string' ? raw.userId : '',
|
|
129
|
+
ticket,
|
|
130
|
+
joinedAt: this.toMillis(raw.joinedAt) ?? 0,
|
|
131
|
+
lastSeenAt: this.toMillis(raw.lastSeenAt) ?? 0,
|
|
132
|
+
expiresAt: this.toMillis(raw.expiresAt),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
isExpired(member, now) {
|
|
136
|
+
if (typeof member.expiresAt === 'number') {
|
|
137
|
+
return member.expiresAt <= now;
|
|
138
|
+
}
|
|
139
|
+
const referenceTime = member.lastSeenAt || member.joinedAt || 0;
|
|
140
|
+
return referenceTime > 0 && referenceTime + ROOM_MEMBER_TTL_MS <= now;
|
|
141
|
+
}
|
|
142
|
+
toMillis(value) {
|
|
143
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
if (typeof value === 'string') {
|
|
147
|
+
const asNumber = Number(value);
|
|
148
|
+
if (Number.isFinite(asNumber)) {
|
|
149
|
+
return asNumber;
|
|
150
|
+
}
|
|
151
|
+
const parsed = Date.parse(value);
|
|
152
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
153
|
+
}
|
|
154
|
+
if (value instanceof Date) {
|
|
155
|
+
return value.getTime();
|
|
156
|
+
}
|
|
157
|
+
if (value &&
|
|
158
|
+
typeof value === 'object' &&
|
|
159
|
+
typeof value.toMillis === 'function') {
|
|
160
|
+
try {
|
|
161
|
+
return value.toMillis();
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { FirebaseApp } from 'firebase/app';
|
|
2
|
+
import type { Auth } from 'firebase/auth';
|
|
3
|
+
import type { Device, DeviceEvent, DiscoveryMode, SessionEvent, SignalingEnvelope, SignalingSession } from '../types';
|
|
4
|
+
import { RtdbPresence, type DiscoveryPresenceScope } from './RtdbPresence';
|
|
5
|
+
export interface BrowserFirestoreSignalingOptions {
|
|
6
|
+
app: FirebaseApp;
|
|
7
|
+
auth: Auth;
|
|
8
|
+
appTag: string;
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
spaceKey?: string;
|
|
11
|
+
discoveryMode?: DiscoveryMode;
|
|
12
|
+
getCurrentUserId: () => string | null;
|
|
13
|
+
getDiscoveryScopeId: () => string | null;
|
|
14
|
+
getLocalDeviceId: () => string;
|
|
15
|
+
getDefaultDeviceName: () => string;
|
|
16
|
+
matchesTag: (device: Pick<Device, 'tag' | 'appTag'>) => boolean;
|
|
17
|
+
normalizeDevice: (raw: Record<string, any>, fallbackId?: string) => Device;
|
|
18
|
+
}
|
|
19
|
+
export declare class BrowserFirestoreSignaling {
|
|
20
|
+
private readonly options;
|
|
21
|
+
private readonly deviceListeners;
|
|
22
|
+
private readonly devicesById;
|
|
23
|
+
private readonly staleDeviceOfflineWrites;
|
|
24
|
+
private readonly nextListenerIdSeed;
|
|
25
|
+
private activeDeviceScopeKey;
|
|
26
|
+
private activeDeviceUnsubscribe;
|
|
27
|
+
private deviceWatchGeneration;
|
|
28
|
+
private namespaceRootPromise;
|
|
29
|
+
private readonly firestore;
|
|
30
|
+
private rtdbPresence;
|
|
31
|
+
private rtdbScope;
|
|
32
|
+
private rtdbPresenceMap;
|
|
33
|
+
private rtdbUnsubscribe;
|
|
34
|
+
private readonly knownTicketVersions;
|
|
35
|
+
constructor(options: BrowserFirestoreSignalingOptions);
|
|
36
|
+
/**
|
|
37
|
+
* Wire in the RTDB presence instance and scope. Called by WasmBridge after
|
|
38
|
+
* auth is established. Must be called before the first onDevicesChange call
|
|
39
|
+
* for RTDB presence to take effect. If not called, falls back to the legacy
|
|
40
|
+
* Firestore onSnapshot watcher (no regression for callers that don't upgrade).
|
|
41
|
+
*/
|
|
42
|
+
setRtdbPresence(rtdbPresence: RtdbPresence, scope: DiscoveryPresenceScope): void;
|
|
43
|
+
/** Stop RTDB subscription and clear scope. Called on sign-out or scope change. */
|
|
44
|
+
stopRtdbPresence(): void;
|
|
45
|
+
searchDevices(excludeNodeId?: string): Promise<Device[]>;
|
|
46
|
+
subscribeDevices(): Promise<ReadableStream<DeviceEvent[]>>;
|
|
47
|
+
onDevicesChange(callback: (devices: Device[]) => void, excludeNodeId?: string): () => void;
|
|
48
|
+
handleScopeChange(): Promise<void>;
|
|
49
|
+
stopAuthScopedActivity(): void;
|
|
50
|
+
updatePresence(localNodeId: string, ticketStr: string, isOnline: boolean, ttlMs: number, metadata?: string, deviceNameOverride?: string): Promise<void>;
|
|
51
|
+
setOffline(localNodeId: string): Promise<void>;
|
|
52
|
+
cleanupStaleDevices(): Promise<void>;
|
|
53
|
+
updateDevice(deviceId: string, updates: {
|
|
54
|
+
deviceName?: string;
|
|
55
|
+
capabilities?: Device['capabilities'];
|
|
56
|
+
metadata?: string;
|
|
57
|
+
}): Promise<void>;
|
|
58
|
+
deleteDevice(deviceId: string): Promise<void>;
|
|
59
|
+
createSession(session: SignalingSession): Promise<void>;
|
|
60
|
+
private shouldUseHostedSessionCallable;
|
|
61
|
+
private createHostedSession;
|
|
62
|
+
updateSession(sessionId: string, updateData: any): Promise<void>;
|
|
63
|
+
sendMessage(targetId: string, payload: string, state?: string, replyPayload?: string): Promise<string>;
|
|
64
|
+
pollMessages(targetId: string): Promise<SignalingEnvelope[]>;
|
|
65
|
+
subscribeSessions(localDeviceId: string): Promise<ReadableStream<SessionEvent[]>>;
|
|
66
|
+
private ensureDeviceWatch;
|
|
67
|
+
/**
|
|
68
|
+
* RTDB-backed device watch (Phase 1).
|
|
69
|
+
*
|
|
70
|
+
* 1. One-time getDocs() to populate devicesById with Firestore tickets.
|
|
71
|
+
* 2. Subscribe to RTDB for the live online/offline boolean overlay.
|
|
72
|
+
* 3. When RTDB fires, merge online state into devicesById and emit.
|
|
73
|
+
* 4. If ticketVersion changed for a device, re-fetch its Firestore doc.
|
|
74
|
+
*/
|
|
75
|
+
private ensureDeviceWatchRtdb;
|
|
76
|
+
/**
|
|
77
|
+
* Merge RTDB online/offline map into devicesById, then emit.
|
|
78
|
+
* Devices present in Firestore cache but absent from RTDB are shown as-is
|
|
79
|
+
* (their Firestore `online` field is the fallback).
|
|
80
|
+
*/
|
|
81
|
+
private applyRtdbOverlay;
|
|
82
|
+
/**
|
|
83
|
+
* Re-fetch specific device docs from Firestore (ticket version changed).
|
|
84
|
+
* Updates devicesById and re-emits.
|
|
85
|
+
*/
|
|
86
|
+
private refetchDeviceTickets;
|
|
87
|
+
refreshDeviceForAutoConnect(deviceId: string): Promise<Device | null>;
|
|
88
|
+
private stopDeviceWatch;
|
|
89
|
+
private emitDevicesToAll;
|
|
90
|
+
private emitDevicesToListener;
|
|
91
|
+
private filterDevices;
|
|
92
|
+
private readCurrentDeviceSnapshot;
|
|
93
|
+
private toDevice;
|
|
94
|
+
private deviceFromSnapshot;
|
|
95
|
+
private markStaleDeviceOffline;
|
|
96
|
+
private toSession;
|
|
97
|
+
private parseMaybeJson;
|
|
98
|
+
private isDeviceExpired;
|
|
99
|
+
private toMillis;
|
|
100
|
+
private getDevicesCollectionRef;
|
|
101
|
+
private getSessionsCollectionRef;
|
|
102
|
+
private getMessagesCollectionRef;
|
|
103
|
+
private getDevicesCollectionPath;
|
|
104
|
+
private getNamespaceRoot;
|
|
105
|
+
private usesPersonalSpaceMode;
|
|
106
|
+
private parseScopedId;
|
|
107
|
+
private getMessageSenderId;
|
|
108
|
+
private createDocumentId;
|
|
109
|
+
private resolveOrCreateLocalDeviceRef;
|
|
110
|
+
private resolveDeviceRef;
|
|
111
|
+
}
|