@myko/core 4.4.1 → 4.4.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/client.d.ts +252 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1316 -0
- package/dist/generated/CancelSubscription.d.ts +7 -0
- package/dist/generated/CancelSubscription.d.ts.map +1 -0
- package/{src/generated/ServerId.ts → dist/generated/CancelSubscription.js} +1 -2
- package/dist/generated/ChildEntities.d.ts +8 -0
- package/dist/generated/ChildEntities.d.ts.map +1 -0
- package/{src/generated/ClientId.ts → dist/generated/ChildEntities.js} +1 -2
- package/dist/generated/ChildEntitiesAllTime.d.ts +8 -0
- package/dist/generated/ChildEntitiesAllTime.d.ts.map +1 -0
- package/{src/generated/MEventType.ts → dist/generated/ChildEntitiesAllTime.js} +1 -2
- package/{src/generated/ClearClientWindbackTime.ts → dist/generated/ClearClientWindbackTime.d.ts} +1 -2
- package/dist/generated/ClearClientWindbackTime.d.ts.map +1 -0
- package/{src/generated/ClientCount.ts → dist/generated/ClearClientWindbackTime.js} +1 -2
- package/dist/generated/ClearClientWindbackTimeArgs.d.ts +2 -0
- package/dist/generated/ClearClientWindbackTimeArgs.d.ts.map +1 -0
- package/dist/generated/ClearClientWindbackTimeArgs.js +2 -0
- package/dist/generated/Client.d.ts +12 -0
- package/dist/generated/Client.d.ts.map +1 -0
- package/dist/generated/Client.js +1 -0
- package/dist/generated/ClientCount.d.ts +4 -0
- package/dist/generated/ClientCount.d.ts.map +1 -0
- package/dist/generated/ClientCount.js +2 -0
- package/dist/generated/ClientId.d.ts +2 -0
- package/dist/generated/ClientId.d.ts.map +1 -0
- package/dist/generated/ClientId.js +2 -0
- package/dist/generated/ClientStatus.d.ts +8 -0
- package/dist/generated/ClientStatus.d.ts.map +1 -0
- package/dist/generated/ClientStatus.js +1 -0
- package/dist/generated/ClientStatusOutput.d.ts +4 -0
- package/dist/generated/ClientStatusOutput.d.ts.map +1 -0
- package/dist/generated/ClientStatusOutput.js +2 -0
- package/dist/generated/CommandError.d.ts +6 -0
- package/dist/generated/CommandError.d.ts.map +1 -0
- package/dist/generated/CommandError.js +2 -0
- package/dist/generated/CommandResponse.d.ts +6 -0
- package/dist/generated/CommandResponse.d.ts.map +1 -0
- package/dist/generated/CommandResponse.js +1 -0
- package/dist/generated/CountAllClients.d.ts +2 -0
- package/dist/generated/CountAllClients.d.ts.map +1 -0
- package/dist/generated/CountAllClients.js +2 -0
- package/dist/generated/CountAllServers.d.ts +2 -0
- package/dist/generated/CountAllServers.d.ts.map +1 -0
- package/dist/generated/CountAllServers.js +2 -0
- package/dist/generated/CountClients.d.ts +3 -0
- package/dist/generated/CountClients.d.ts.map +1 -0
- package/dist/generated/CountClients.js +1 -0
- package/dist/generated/CountServers.d.ts +3 -0
- package/dist/generated/CountServers.d.ts.map +1 -0
- package/dist/generated/CountServers.js +1 -0
- package/dist/generated/DeleteClient.d.ts +8 -0
- package/dist/generated/DeleteClient.d.ts.map +1 -0
- package/dist/generated/DeleteClient.js +1 -0
- package/dist/generated/DeleteClientArgs.d.ts +5 -0
- package/dist/generated/DeleteClientArgs.d.ts.map +1 -0
- package/dist/generated/DeleteClientArgs.js +1 -0
- package/dist/generated/DeleteClientResult.d.ts +7 -0
- package/dist/generated/DeleteClientResult.d.ts.map +1 -0
- package/dist/generated/DeleteClientResult.js +2 -0
- package/dist/generated/DeleteClients.d.ts +8 -0
- package/dist/generated/DeleteClients.d.ts.map +1 -0
- package/dist/generated/DeleteClients.js +1 -0
- package/dist/generated/DeleteClientsArgs.d.ts +5 -0
- package/dist/generated/DeleteClientsArgs.d.ts.map +1 -0
- package/dist/generated/DeleteClientsArgs.js +1 -0
- package/dist/generated/DeleteClientsResult.d.ts +7 -0
- package/dist/generated/DeleteClientsResult.d.ts.map +1 -0
- package/dist/generated/DeleteClientsResult.js +2 -0
- package/dist/generated/DeleteServer.d.ts +8 -0
- package/dist/generated/DeleteServer.d.ts.map +1 -0
- package/dist/generated/DeleteServer.js +1 -0
- package/dist/generated/DeleteServerArgs.d.ts +5 -0
- package/dist/generated/DeleteServerArgs.d.ts.map +1 -0
- package/dist/generated/DeleteServerArgs.js +1 -0
- package/dist/generated/DeleteServerResult.d.ts +7 -0
- package/dist/generated/DeleteServerResult.d.ts.map +1 -0
- package/dist/generated/DeleteServerResult.js +2 -0
- package/dist/generated/DeleteServers.d.ts +8 -0
- package/dist/generated/DeleteServers.d.ts.map +1 -0
- package/dist/generated/DeleteServers.js +1 -0
- package/dist/generated/DeleteServersArgs.d.ts +5 -0
- package/dist/generated/DeleteServersArgs.d.ts.map +1 -0
- package/dist/generated/DeleteServersArgs.js +1 -0
- package/dist/generated/DeleteServersResult.d.ts +7 -0
- package/dist/generated/DeleteServersResult.d.ts.map +1 -0
- package/dist/generated/DeleteServersResult.js +2 -0
- package/dist/generated/EntitySearch.d.ts +21 -0
- package/dist/generated/EntitySearch.d.ts.map +1 -0
- package/dist/generated/EntitySearch.js +2 -0
- package/dist/generated/EntitySearchResult.d.ts +10 -0
- package/dist/generated/EntitySearchResult.d.ts.map +1 -0
- package/dist/generated/EntitySearchResult.js +2 -0
- package/dist/generated/EntitySnapshotDifference.d.ts +8 -0
- package/dist/generated/EntitySnapshotDifference.d.ts.map +1 -0
- package/dist/generated/EntitySnapshotDifference.js +2 -0
- package/dist/generated/EntitySnapshotDifferenceData.d.ts +10 -0
- package/dist/generated/EntitySnapshotDifferenceData.d.ts.map +1 -0
- package/dist/generated/EntitySnapshotDifferenceData.js +1 -0
- package/dist/generated/EntityTreeExport.d.ts +27 -0
- package/dist/generated/EntityTreeExport.d.ts.map +1 -0
- package/dist/generated/EntityTreeExport.js +1 -0
- package/dist/generated/EventContainer.d.ts +9 -0
- package/dist/generated/EventContainer.d.ts.map +1 -0
- package/dist/generated/EventContainer.js +1 -0
- package/dist/generated/EventOptions.d.ts +21 -0
- package/dist/generated/EventOptions.d.ts.map +1 -0
- package/dist/generated/EventOptions.js +2 -0
- package/dist/generated/EventsForTransaction.d.ts +7 -0
- package/dist/generated/EventsForTransaction.d.ts.map +1 -0
- package/dist/generated/EventsForTransaction.js +2 -0
- package/dist/generated/ExportEntityTree.d.ts +24 -0
- package/dist/generated/ExportEntityTree.d.ts.map +1 -0
- package/dist/generated/ExportEntityTree.js +2 -0
- package/dist/generated/ExportedEntity.d.ts +15 -0
- package/dist/generated/ExportedEntity.d.ts.map +1 -0
- package/dist/generated/ExportedEntity.js +1 -0
- package/dist/generated/FullChildEntities.d.ts +8 -0
- package/dist/generated/FullChildEntities.d.ts.map +1 -0
- package/dist/generated/FullChildEntities.js +2 -0
- package/dist/generated/GetAllClients.d.ts +2 -0
- package/dist/generated/GetAllClients.d.ts.map +1 -0
- package/dist/generated/GetAllClients.js +2 -0
- package/dist/generated/GetAllServers.d.ts +2 -0
- package/dist/generated/GetAllServers.d.ts.map +1 -0
- package/dist/generated/GetAllServers.js +2 -0
- package/dist/generated/GetClientById.d.ts +5 -0
- package/dist/generated/GetClientById.d.ts.map +1 -0
- package/dist/generated/GetClientById.js +1 -0
- package/dist/generated/GetClientsByIds.d.ts +5 -0
- package/dist/generated/GetClientsByIds.d.ts.map +1 -0
- package/dist/generated/GetClientsByIds.js +1 -0
- package/dist/generated/GetClientsByQuery.d.ts +3 -0
- package/dist/generated/GetClientsByQuery.d.ts.map +1 -0
- package/dist/generated/GetClientsByQuery.js +1 -0
- package/dist/generated/GetConnectedServer.d.ts +2 -0
- package/dist/generated/GetConnectedServer.d.ts.map +1 -0
- package/dist/generated/GetConnectedServer.js +2 -0
- package/dist/generated/GetItemsByTypeAndIds.d.ts +14 -0
- package/dist/generated/GetItemsByTypeAndIds.d.ts.map +1 -0
- package/dist/generated/GetItemsByTypeAndIds.js +2 -0
- package/dist/generated/GetPeerServers.d.ts +2 -0
- package/dist/generated/GetPeerServers.d.ts.map +1 -0
- package/dist/generated/GetPeerServers.js +2 -0
- package/{src/generated/GetPersistHealth.ts → dist/generated/GetPersistHealth.d.ts} +1 -2
- package/dist/generated/GetPersistHealth.d.ts.map +1 -0
- package/dist/generated/GetPersistHealth.js +2 -0
- package/dist/generated/GetServerById.d.ts +5 -0
- package/dist/generated/GetServerById.d.ts.map +1 -0
- package/dist/generated/GetServerById.js +1 -0
- package/dist/generated/GetServersByIds.d.ts +5 -0
- package/dist/generated/GetServersByIds.d.ts.map +1 -0
- package/dist/generated/GetServersByIds.js +1 -0
- package/dist/generated/GetServersByQuery.d.ts +3 -0
- package/dist/generated/GetServersByQuery.d.ts.map +1 -0
- package/dist/generated/GetServersByQuery.js +1 -0
- package/dist/generated/ImportItems.d.ts +19 -0
- package/dist/generated/ImportItems.d.ts.map +1 -0
- package/dist/generated/ImportItems.js +1 -0
- package/dist/generated/ImportItemsArgs.d.ts +13 -0
- package/dist/generated/ImportItemsArgs.d.ts.map +1 -0
- package/dist/generated/ImportItemsArgs.js +1 -0
- package/dist/generated/ItemStub.d.ts +10 -0
- package/dist/generated/ItemStub.d.ts.map +1 -0
- package/dist/generated/ItemStub.js +2 -0
- package/{src/generated/LogLevel.ts → dist/generated/LogLevel.d.ts} +1 -2
- package/dist/generated/LogLevel.d.ts.map +1 -0
- package/dist/generated/LogLevel.js +2 -0
- package/{src/generated/Loggers.ts → dist/generated/Loggers.d.ts} +1 -2
- package/dist/generated/Loggers.d.ts.map +1 -0
- package/dist/generated/Loggers.js +2 -0
- package/dist/generated/MEvent.d.ts +16 -0
- package/dist/generated/MEvent.d.ts.map +1 -0
- package/dist/generated/MEvent.js +1 -0
- package/dist/generated/MEventType.d.ts +2 -0
- package/dist/generated/MEventType.d.ts.map +1 -0
- package/dist/generated/MEventType.js +2 -0
- package/dist/generated/MykoMessage.d.ts +86 -0
- package/dist/generated/MykoMessage.d.ts.map +1 -0
- package/dist/generated/MykoMessage.js +1 -0
- package/dist/generated/PartialClient.d.ts +12 -0
- package/dist/generated/PartialClient.d.ts.map +1 -0
- package/dist/generated/PartialClient.js +1 -0
- package/dist/generated/PartialServer.d.ts +9 -0
- package/dist/generated/PartialServer.d.ts.map +1 -0
- package/dist/generated/PartialServer.js +1 -0
- package/dist/generated/PeerAlive.d.ts +8 -0
- package/dist/generated/PeerAlive.d.ts.map +1 -0
- package/dist/generated/PeerAlive.js +2 -0
- package/dist/generated/PersistHealthStatus.d.ts +34 -0
- package/dist/generated/PersistHealthStatus.d.ts.map +1 -0
- package/dist/generated/PersistHealthStatus.js +2 -0
- package/dist/generated/PingData.d.ts +14 -0
- package/dist/generated/PingData.d.ts.map +1 -0
- package/dist/generated/PingData.js +2 -0
- package/dist/generated/QueryError.d.ts +6 -0
- package/dist/generated/QueryError.d.ts.map +1 -0
- package/dist/generated/QueryError.js +2 -0
- package/dist/generated/QueryWindow.d.ts +5 -0
- package/dist/generated/QueryWindow.d.ts.map +1 -0
- package/dist/generated/QueryWindow.js +2 -0
- package/dist/generated/QueryWindowUpdate.d.ts +6 -0
- package/dist/generated/QueryWindowUpdate.d.ts.map +1 -0
- package/dist/generated/QueryWindowUpdate.js +1 -0
- package/dist/generated/ReportError.d.ts +6 -0
- package/dist/generated/ReportError.d.ts.map +1 -0
- package/dist/generated/ReportError.js +2 -0
- package/dist/generated/ReportResponse.d.ts +6 -0
- package/dist/generated/ReportResponse.d.ts.map +1 -0
- package/dist/generated/ReportResponse.js +1 -0
- package/dist/generated/Server.d.ts +9 -0
- package/dist/generated/Server.d.ts.map +1 -0
- package/dist/generated/Server.js +1 -0
- package/dist/generated/ServerCount.d.ts +4 -0
- package/dist/generated/ServerCount.d.ts.map +1 -0
- package/dist/generated/ServerCount.js +2 -0
- package/dist/generated/ServerId.d.ts +2 -0
- package/dist/generated/ServerId.d.ts.map +1 -0
- package/dist/generated/ServerId.js +2 -0
- package/dist/generated/ServerLogLevel.d.ts +7 -0
- package/dist/generated/ServerLogLevel.d.ts.map +1 -0
- package/dist/generated/ServerLogLevel.js +2 -0
- package/{src/generated/ServerStats.ts → dist/generated/ServerStats.d.ts} +1 -2
- package/dist/generated/ServerStats.d.ts.map +1 -0
- package/dist/generated/ServerStats.js +2 -0
- package/dist/generated/ServerStatsOutput.d.ts +20 -0
- package/dist/generated/ServerStatsOutput.d.ts.map +1 -0
- package/dist/generated/ServerStatsOutput.js +1 -0
- package/dist/generated/SetClientWindbackTime.d.ts +11 -0
- package/dist/generated/SetClientWindbackTime.d.ts.map +1 -0
- package/dist/generated/SetClientWindbackTime.js +2 -0
- package/dist/generated/SetClientWindbackTimeArgs.d.ts +7 -0
- package/dist/generated/SetClientWindbackTimeArgs.d.ts.map +1 -0
- package/dist/generated/SetClientWindbackTimeArgs.js +2 -0
- package/dist/generated/SetLogLevel.d.ts +9 -0
- package/dist/generated/SetLogLevel.d.ts.map +1 -0
- package/dist/generated/SetLogLevel.js +1 -0
- package/dist/generated/SetLogLevelArgs.d.ts +6 -0
- package/dist/generated/SetLogLevelArgs.d.ts.map +1 -0
- package/dist/generated/SetLogLevelArgs.js +1 -0
- package/dist/generated/ViewError.d.ts +6 -0
- package/dist/generated/ViewError.d.ts.map +1 -0
- package/dist/generated/ViewError.js +2 -0
- package/dist/generated/ViewWindowUpdate.d.ts +6 -0
- package/dist/generated/ViewWindowUpdate.d.ts.map +1 -0
- package/dist/generated/ViewWindowUpdate.js +1 -0
- package/dist/generated/WindbackStatus.d.ts +2 -0
- package/dist/generated/WindbackStatus.d.ts.map +1 -0
- package/dist/generated/WindbackStatus.js +2 -0
- package/dist/generated/WindbackStatusOutput.d.ts +11 -0
- package/dist/generated/WindbackStatusOutput.d.ts.map +1 -0
- package/dist/generated/WindbackStatusOutput.js +2 -0
- package/dist/generated/WrappedCommand.d.ts +6 -0
- package/dist/generated/WrappedCommand.d.ts.map +1 -0
- package/dist/generated/WrappedCommand.js +1 -0
- package/dist/generated/WrappedItem.d.ts +9 -0
- package/dist/generated/WrappedItem.d.ts.map +1 -0
- package/dist/generated/WrappedItem.js +1 -0
- package/dist/generated/WrappedQuery.d.ts +9 -0
- package/dist/generated/WrappedQuery.d.ts.map +1 -0
- package/dist/generated/WrappedQuery.js +1 -0
- package/dist/generated/WrappedReport.d.ts +6 -0
- package/dist/generated/WrappedReport.d.ts.map +1 -0
- package/dist/generated/WrappedReport.js +1 -0
- package/dist/generated/WrappedView.d.ts +9 -0
- package/dist/generated/WrappedView.d.ts.map +1 -0
- package/dist/generated/WrappedView.js +1 -0
- package/dist/generated/index.d.ts +421 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/dist/generated/index.js +349 -0
- package/dist/generated/serde_json/JsonValue.d.ts +4 -0
- package/dist/generated/serde_json/JsonValue.d.ts.map +1 -0
- package/dist/generated/serde_json/JsonValue.js +2 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +68 -0
- package/package.json +8 -4
- package/src/client.ts +0 -1851
- package/src/generated/CancelSubscription.ts +0 -6
- package/src/generated/ChildEntities.ts +0 -6
- package/src/generated/ChildEntitiesAllTime.ts +0 -6
- package/src/generated/ClearClientWindbackTimeArgs.ts +0 -3
- package/src/generated/Client.ts +0 -10
- package/src/generated/ClientStatus.ts +0 -7
- package/src/generated/ClientStatusOutput.ts +0 -3
- package/src/generated/CommandError.ts +0 -3
- package/src/generated/CommandResponse.ts +0 -4
- package/src/generated/CountAllClients.ts +0 -3
- package/src/generated/CountAllServers.ts +0 -3
- package/src/generated/CountClients.ts +0 -4
- package/src/generated/CountServers.ts +0 -4
- package/src/generated/DeleteClient.ts +0 -7
- package/src/generated/DeleteClientArgs.ts +0 -4
- package/src/generated/DeleteClientResult.ts +0 -6
- package/src/generated/DeleteClients.ts +0 -7
- package/src/generated/DeleteClientsArgs.ts +0 -4
- package/src/generated/DeleteClientsResult.ts +0 -6
- package/src/generated/DeleteServer.ts +0 -7
- package/src/generated/DeleteServerArgs.ts +0 -4
- package/src/generated/DeleteServerResult.ts +0 -6
- package/src/generated/DeleteServers.ts +0 -7
- package/src/generated/DeleteServersArgs.ts +0 -4
- package/src/generated/DeleteServersResult.ts +0 -6
- package/src/generated/EntitySearch.ts +0 -21
- package/src/generated/EntitySearchResult.ts +0 -10
- package/src/generated/EntitySnapshotDifference.ts +0 -6
- package/src/generated/EntitySnapshotDifferenceData.ts +0 -7
- package/src/generated/EntityTreeExport.ts +0 -27
- package/src/generated/EventContainer.ts +0 -7
- package/src/generated/EventOptions.ts +0 -21
- package/src/generated/EventsForTransaction.ts +0 -6
- package/src/generated/ExportEntityTree.ts +0 -24
- package/src/generated/ExportedEntity.ts +0 -15
- package/src/generated/FullChildEntities.ts +0 -6
- package/src/generated/GetAllClients.ts +0 -3
- package/src/generated/GetAllServers.ts +0 -3
- package/src/generated/GetClientById.ts +0 -4
- package/src/generated/GetClientsByIds.ts +0 -4
- package/src/generated/GetClientsByQuery.ts +0 -4
- package/src/generated/GetConnectedServer.ts +0 -3
- package/src/generated/GetItemsByTypeAndIds.ts +0 -14
- package/src/generated/GetPeerServers.ts +0 -3
- package/src/generated/GetServerById.ts +0 -4
- package/src/generated/GetServersByIds.ts +0 -4
- package/src/generated/GetServersByQuery.ts +0 -4
- package/src/generated/ImportItems.ts +0 -19
- package/src/generated/ImportItemsArgs.ts +0 -13
- package/src/generated/ItemStub.ts +0 -7
- package/src/generated/MEvent.ts +0 -10
- package/src/generated/MykoMessage.ts +0 -19
- package/src/generated/PartialClient.ts +0 -10
- package/src/generated/PartialServer.ts +0 -4
- package/src/generated/PeerAlive.ts +0 -7
- package/src/generated/PersistHealthStatus.ts +0 -34
- package/src/generated/PingData.ts +0 -14
- package/src/generated/QueryError.ts +0 -3
- package/src/generated/QueryWindow.ts +0 -3
- package/src/generated/QueryWindowUpdate.ts +0 -4
- package/src/generated/ReportError.ts +0 -3
- package/src/generated/ReportResponse.ts +0 -4
- package/src/generated/Server.ts +0 -4
- package/src/generated/ServerCount.ts +0 -3
- package/src/generated/ServerLogLevel.ts +0 -6
- package/src/generated/ServerStatsOutput.ts +0 -20
- package/src/generated/SetClientWindbackTime.ts +0 -11
- package/src/generated/SetClientWindbackTimeArgs.ts +0 -7
- package/src/generated/SetLogLevel.ts +0 -7
- package/src/generated/SetLogLevelArgs.ts +0 -4
- package/src/generated/ViewError.ts +0 -3
- package/src/generated/ViewWindowUpdate.ts +0 -4
- package/src/generated/WindbackStatus.ts +0 -3
- package/src/generated/WindbackStatusOutput.ts +0 -11
- package/src/generated/WrappedCommand.ts +0 -4
- package/src/generated/WrappedItem.ts +0 -7
- package/src/generated/WrappedQuery.ts +0 -5
- package/src/generated/WrappedReport.ts +0 -4
- package/src/generated/WrappedView.ts +0 -5
- package/src/generated/index.ts +0 -580
- package/src/generated/serde_json/JsonValue.ts +0 -3
- package/src/index.ts +0 -128
package/src/client.ts
DELETED
|
@@ -1,1851 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure TypeScript WebSocket client for Myko servers
|
|
3
|
-
*
|
|
4
|
-
* Maintains connections to all known servers for instant failover.
|
|
5
|
-
* When the current server disconnects, instantly switches to another open connection.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
GetPeerServers,
|
|
10
|
-
MykoEvent,
|
|
11
|
-
type JsonValue,
|
|
12
|
-
type MEvent,
|
|
13
|
-
type MykoMessage,
|
|
14
|
-
type PingData,
|
|
15
|
-
type Server,
|
|
16
|
-
type WrappedItem,
|
|
17
|
-
type WrappedQuery,
|
|
18
|
-
type WrappedReport,
|
|
19
|
-
type WrappedView,
|
|
20
|
-
} from './generated'
|
|
21
|
-
import { Packr, Unpackr } from 'msgpackr'
|
|
22
|
-
import {
|
|
23
|
-
bufferCount,
|
|
24
|
-
bufferTime,
|
|
25
|
-
catchError,
|
|
26
|
-
combineLatest,
|
|
27
|
-
filter,
|
|
28
|
-
finalize,
|
|
29
|
-
firstValueFrom,
|
|
30
|
-
interval,
|
|
31
|
-
map,
|
|
32
|
-
merge,
|
|
33
|
-
Observable,
|
|
34
|
-
of,
|
|
35
|
-
ReplaySubject,
|
|
36
|
-
scan,
|
|
37
|
-
shareReplay,
|
|
38
|
-
Subject,
|
|
39
|
-
Subscription,
|
|
40
|
-
switchMap,
|
|
41
|
-
} from 'rxjs'
|
|
42
|
-
import { v4 as uuid } from 'uuid'
|
|
43
|
-
|
|
44
|
-
// msgpackr defaults can emit extension types for values that don't exist in JSON (notably
|
|
45
|
-
// `undefined`). Our Rust server deserializes msgpack into `serde_json::Value`, so ensure we
|
|
46
|
-
// encode `undefined` as nil/null instead of an extension.
|
|
47
|
-
const packr = new Packr({ encodeUndefinedAsNil: true })
|
|
48
|
-
const unpackr = new Unpackr({})
|
|
49
|
-
const EVENT_BATCH = 'ws:m:event-batch'
|
|
50
|
-
|
|
51
|
-
function stableStringify(value: unknown): string | null {
|
|
52
|
-
const seen = new WeakSet<object>()
|
|
53
|
-
try {
|
|
54
|
-
return JSON.stringify(value, (_key, raw) => {
|
|
55
|
-
if (typeof raw === 'bigint') return `__bigint:${raw.toString()}`
|
|
56
|
-
if (!raw || typeof raw !== 'object') return raw
|
|
57
|
-
if (seen.has(raw)) return '__circular__'
|
|
58
|
-
seen.add(raw)
|
|
59
|
-
if (Array.isArray(raw)) return raw
|
|
60
|
-
const sorted: Record<string, unknown> = {}
|
|
61
|
-
for (const key of Object.keys(raw as Record<string, unknown>).sort()) {
|
|
62
|
-
sorted[key] = (raw as Record<string, unknown>)[key]
|
|
63
|
-
}
|
|
64
|
-
return sorted
|
|
65
|
-
})
|
|
66
|
-
} catch {
|
|
67
|
-
return null
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Union type for error event names */
|
|
72
|
-
export type MykoErrorEvent =
|
|
73
|
-
| typeof MykoEvent.QueryError
|
|
74
|
-
| typeof MykoEvent.ViewError
|
|
75
|
-
| typeof MykoEvent.CommandError
|
|
76
|
-
| typeof MykoEvent.ReportError
|
|
77
|
-
|
|
78
|
-
/** Error types from server */
|
|
79
|
-
export type MykoError = {
|
|
80
|
-
event: MykoErrorEvent
|
|
81
|
-
tx: string
|
|
82
|
-
message: string
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Client statistics */
|
|
86
|
-
export type ClientStats = {
|
|
87
|
-
ping: number
|
|
88
|
-
mpsDown: number
|
|
89
|
-
mpsUp: number
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Connection status */
|
|
93
|
-
export enum ConnectionStatus {
|
|
94
|
-
Connected = 'Connected',
|
|
95
|
-
Disconnected = 'Disconnected',
|
|
96
|
-
Connecting = 'Connecting',
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Wire protocol for encoding messages */
|
|
100
|
-
export enum MykoProtocol {
|
|
101
|
-
JSON = 'JSON',
|
|
102
|
-
MSGPACK = 'MSGPACK',
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
type ConnectionLogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug' | 'verbose'
|
|
106
|
-
|
|
107
|
-
/** Query class interface */
|
|
108
|
-
export interface Query<T> {
|
|
109
|
-
readonly queryId: string
|
|
110
|
-
readonly queryItemType: string
|
|
111
|
-
readonly query: Record<string, unknown>
|
|
112
|
-
readonly $res?: () => T[]
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** View class interface */
|
|
116
|
-
export interface View<T> {
|
|
117
|
-
readonly viewId: string
|
|
118
|
-
readonly viewItemType: string
|
|
119
|
-
readonly view: Record<string, unknown>
|
|
120
|
-
readonly $res?: () => T[]
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Report class interface */
|
|
124
|
-
export interface Report<T> {
|
|
125
|
-
readonly reportId: string
|
|
126
|
-
readonly report: Record<string, unknown>
|
|
127
|
-
readonly $res?: () => T
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Command class interface */
|
|
131
|
-
export interface Command<T> {
|
|
132
|
-
readonly commandId: string
|
|
133
|
-
readonly command: Record<string, unknown>
|
|
134
|
-
readonly $res?: () => T
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Extract result type from a query */
|
|
138
|
-
export type QueryResult<Q> = Q extends Query<infer R> ? R[] : unknown[]
|
|
139
|
-
|
|
140
|
-
/** Extract item type from a query */
|
|
141
|
-
export type QueryItem<Q> = Q extends Query<infer R> ? R : unknown
|
|
142
|
-
|
|
143
|
-
/** Extract item type from a view */
|
|
144
|
-
export type ViewItem<V> = V extends View<infer R> ? R : unknown
|
|
145
|
-
|
|
146
|
-
/** Extract result type from a view */
|
|
147
|
-
export type ViewResult<V> = V extends View<infer R> ? R[] : unknown[]
|
|
148
|
-
|
|
149
|
-
/** Extract result type from a report */
|
|
150
|
-
export type ReportResult<R> = R extends Report<infer T> ? T : unknown
|
|
151
|
-
|
|
152
|
-
/** Extract result type from a command */
|
|
153
|
-
export type CommandResult<C> = C extends Command<infer T> ? T : unknown
|
|
154
|
-
|
|
155
|
-
/** Diff event for incremental query updates */
|
|
156
|
-
export type QueryDiff<T> = {
|
|
157
|
-
sequence: bigint
|
|
158
|
-
deletes: string[]
|
|
159
|
-
upserts: T[]
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export type QueryWindow = {
|
|
163
|
-
offset: number
|
|
164
|
-
limit: number
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export type QueryWatchOptions = {
|
|
168
|
-
window?: QueryWindow | null
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export type QueryWindowInfo = {
|
|
172
|
-
totalCount: number | null
|
|
173
|
-
window: QueryWindow | null
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function queryCacheKey(
|
|
177
|
-
query: Pick<Query<unknown>, 'queryId' | 'query'>,
|
|
178
|
-
options?: QueryWatchOptions,
|
|
179
|
-
): string {
|
|
180
|
-
const queryPayload = stableStringify(query.query) ?? '__unstable_query__'
|
|
181
|
-
const windowPayload = stableStringify(options?.window ?? null) ?? '__unstable_window__'
|
|
182
|
-
return `query:${query.queryId}:${queryPayload}:${windowPayload}`
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function viewCacheKey(
|
|
186
|
-
view: Pick<View<unknown>, 'viewId' | 'view'>,
|
|
187
|
-
options?: QueryWatchOptions,
|
|
188
|
-
): string {
|
|
189
|
-
const viewPayload = stableStringify(view.view) ?? '__unstable_view__'
|
|
190
|
-
const windowPayload = stableStringify(options?.window ?? null) ?? '__unstable_window__'
|
|
191
|
-
return `view:${view.viewId}:${viewPayload}:${windowPayload}`
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function reportCacheKey(report: Pick<Report<unknown>, 'reportId' | 'report'>): string {
|
|
195
|
-
const payload = stableStringify(report.report) ?? '__unstable_report__'
|
|
196
|
-
return `report:${report.reportId}:${payload}`
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Message type aliases
|
|
200
|
-
type QueryResponseMessage = Extract<
|
|
201
|
-
MykoMessage,
|
|
202
|
-
{ event: typeof MykoEvent.QueryResponse }
|
|
203
|
-
>
|
|
204
|
-
type ReportResponseMessage = Extract<
|
|
205
|
-
MykoMessage,
|
|
206
|
-
{ event: typeof MykoEvent.ReportResponse }
|
|
207
|
-
>
|
|
208
|
-
type CommandResponseMessage = Extract<
|
|
209
|
-
MykoMessage,
|
|
210
|
-
{ event: typeof MykoEvent.CommandResponse }
|
|
211
|
-
>
|
|
212
|
-
type CommandErrorMessage = Extract<
|
|
213
|
-
MykoMessage,
|
|
214
|
-
{ event: typeof MykoEvent.CommandError }
|
|
215
|
-
>
|
|
216
|
-
type QueryErrorMessage = Extract<
|
|
217
|
-
MykoMessage,
|
|
218
|
-
{ event: typeof MykoEvent.QueryError }
|
|
219
|
-
>
|
|
220
|
-
type ReportErrorMessage = Extract<
|
|
221
|
-
MykoMessage,
|
|
222
|
-
{ event: typeof MykoEvent.ReportError }
|
|
223
|
-
>
|
|
224
|
-
type PingMessage = Extract<MykoMessage, { event: typeof MykoEvent.Ping }>
|
|
225
|
-
type CommandIncomingMessage = Extract<
|
|
226
|
-
MykoMessage,
|
|
227
|
-
{ event: typeof MykoEvent.Command }
|
|
228
|
-
>
|
|
229
|
-
|
|
230
|
-
interface ManagedSocket {
|
|
231
|
-
ws: WebSocket
|
|
232
|
-
address: string
|
|
233
|
-
endpointKey: string
|
|
234
|
-
reconnectOnClose: boolean
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Reactive WebSocket client for Myko servers with automatic failover.
|
|
239
|
-
*/
|
|
240
|
-
export class MykoClient {
|
|
241
|
-
// Socket management
|
|
242
|
-
private sockets = new Map<string, ManagedSocket>()
|
|
243
|
-
private reconnectTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
244
|
-
private endpointSockets = new Map<string, string>()
|
|
245
|
-
// Main server: the socket used for outbound sends.
|
|
246
|
-
// Other open sockets are warm standbys for failover.
|
|
247
|
-
private currentServer: string | null = null
|
|
248
|
-
private shouldReconnect = true
|
|
249
|
-
|
|
250
|
-
// Message routing
|
|
251
|
-
private queryResponses = new Subject<QueryResponseMessage>()
|
|
252
|
-
private reportResponses = new Subject<ReportResponseMessage>()
|
|
253
|
-
private commandResponses = new Subject<CommandResponseMessage>()
|
|
254
|
-
private commandErrors = new Subject<CommandErrorMessage>()
|
|
255
|
-
private queryErrors = new Subject<QueryErrorMessage>()
|
|
256
|
-
private reportErrors = new Subject<ReportErrorMessage>()
|
|
257
|
-
private pingResponses = new Subject<PingMessage>()
|
|
258
|
-
private commandIncoming = new Subject<CommandIncomingMessage>()
|
|
259
|
-
|
|
260
|
-
// State observables
|
|
261
|
-
private connectionStatusSubject = new ReplaySubject<ConnectionStatus>(1)
|
|
262
|
-
private currentServerSubject = new ReplaySubject<string | null>(1)
|
|
263
|
-
|
|
264
|
-
// Subscription tracking
|
|
265
|
-
private activeQueries = new Map<string, WrappedQuery>()
|
|
266
|
-
private activeViews = new Map<string, WrappedView>()
|
|
267
|
-
private activeReports = new Map<string, WrappedReport>()
|
|
268
|
-
private activeQueryNames = new Map<string, string>()
|
|
269
|
-
private activeViewNames = new Map<string, string>()
|
|
270
|
-
private activeReportNames = new Map<string, string>()
|
|
271
|
-
private sharedQueries = new Map<string, Observable<unknown>>()
|
|
272
|
-
private sharedViews = new Map<string, Observable<unknown>>()
|
|
273
|
-
private sharedQueryDiffs = new Map<string, Observable<unknown>>()
|
|
274
|
-
private sharedViewDiffs = new Map<string, Observable<unknown>>()
|
|
275
|
-
private sharedReports = new Map<string, Observable<unknown>>()
|
|
276
|
-
private subscriptionStartMs = new Map<string, number>()
|
|
277
|
-
private firstResponseLogged = new Set<string>()
|
|
278
|
-
private messageQueue: MykoMessage[] = []
|
|
279
|
-
private pendingEventBatch: MEvent[] = []
|
|
280
|
-
private eventBatchFlushScheduled = false
|
|
281
|
-
private readonly eventBatchMaxSize = 256
|
|
282
|
-
private connectionLogLevelThreshold: ConnectionLogLevel =
|
|
283
|
-
MykoClient.resolveDefaultConnectionLogLevel()
|
|
284
|
-
|
|
285
|
-
// Stats
|
|
286
|
-
private downMsgCounter = new Subject<void>()
|
|
287
|
-
private upMsgCounter = new Subject<void>()
|
|
288
|
-
|
|
289
|
-
// Auth & peer discovery
|
|
290
|
-
private userToken: string | null = null
|
|
291
|
-
private peerDiscoveryEnabled = true
|
|
292
|
-
private peerDiscoverySubscription: Subscription | null = null
|
|
293
|
-
private useSecureWebSocket = false
|
|
294
|
-
|
|
295
|
-
// Protocol defaults to JSON for maximum compatibility (no msgpack extensions, bigint issues, etc).
|
|
296
|
-
private protocol: MykoProtocol = MykoProtocol.JSON
|
|
297
|
-
|
|
298
|
-
constructor() {
|
|
299
|
-
this.setConnectionStatus(ConnectionStatus.Disconnected, 'init')
|
|
300
|
-
this.setCurrentServer(null, 'init')
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/** Set connection log verbosity at runtime. */
|
|
304
|
-
setConnectionLogLevel(level: ConnectionLogLevel): void {
|
|
305
|
-
this.connectionLogLevelThreshold = level
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/** Set the wire protocol (JSON or MSGPACK). Default is MSGPACK. */
|
|
309
|
-
setProtocol(protocol: MykoProtocol): void {
|
|
310
|
-
this.protocol = protocol
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
314
|
-
// Connection Management
|
|
315
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
316
|
-
|
|
317
|
-
/** Set a single server address, clearing any existing connections */
|
|
318
|
-
setAddress(address: string | null): void {
|
|
319
|
-
this.shouldReconnect = true // Re-enable autoreconnect when setting new address
|
|
320
|
-
this.closeAllSockets()
|
|
321
|
-
if (address) {
|
|
322
|
-
this.useSecureWebSocket = address.startsWith('wss://')
|
|
323
|
-
this.createSocket(address, true)
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/** Set multiple server addresses, clearing any existing connections */
|
|
328
|
-
setAddresses(addresses: string[]): void {
|
|
329
|
-
this.shouldReconnect = true // Re-enable autoreconnect when setting new addresses
|
|
330
|
-
this.closeAllSockets()
|
|
331
|
-
for (const addr of addresses) {
|
|
332
|
-
this.createSocket(addr, true)
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/** Add additional servers (connects immediately) */
|
|
337
|
-
addServers(addresses: string[], reconnectOnClose = true): void {
|
|
338
|
-
this.shouldReconnect = true // Re-enable autoreconnect when adding servers
|
|
339
|
-
for (const addr of addresses) {
|
|
340
|
-
if (!this.hasConnectionTo(addr)) {
|
|
341
|
-
this.createSocket(addr, reconnectOnClose)
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/** Disconnect from all servers */
|
|
347
|
-
disconnect(): void {
|
|
348
|
-
this.shouldReconnect = false
|
|
349
|
-
this.stopPeerDiscovery()
|
|
350
|
-
this.closeAllSockets()
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/** Get the currently active server address */
|
|
354
|
-
getCurrentServer(): string | null {
|
|
355
|
-
return this.currentServer
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/** Get the current main server address used for outbound sends */
|
|
359
|
-
getMainServer(): string | null {
|
|
360
|
-
return this.currentServer
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/** Get all server addresses */
|
|
364
|
-
getServers(): string[] {
|
|
365
|
-
return Array.from(this.sockets.values()).map((m) => m.address)
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/** Get addresses of all open connections */
|
|
369
|
-
getOpenServers(): string[] {
|
|
370
|
-
return Array.from(this.sockets.values())
|
|
371
|
-
.filter((m) => m.ws.readyState === WebSocket.OPEN)
|
|
372
|
-
.map((m) => m.address)
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/** Observable of current server changes */
|
|
376
|
-
get currentServer$(): Observable<string | null> {
|
|
377
|
-
return this.currentServerSubject.asObservable()
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/** Observable of main server changes */
|
|
381
|
-
get mainServer$(): Observable<string | null> {
|
|
382
|
-
return this.currentServer$
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/** Get current connection status */
|
|
386
|
-
getConnectionStatus(): ConnectionStatus {
|
|
387
|
-
if (this.currentServer) return ConnectionStatus.Connected
|
|
388
|
-
for (const m of this.sockets.values()) {
|
|
389
|
-
if (m.ws.readyState === WebSocket.CONNECTING)
|
|
390
|
-
return ConnectionStatus.Connecting
|
|
391
|
-
}
|
|
392
|
-
return ConnectionStatus.Disconnected
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/** Observable of connection status changes */
|
|
396
|
-
get connectionStatus$(): Observable<ConnectionStatus> {
|
|
397
|
-
return this.connectionStatusSubject.asObservable()
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/** Observable of incoming command messages (ws:m:command) from the server */
|
|
401
|
-
get commandIncoming$(): Observable<CommandIncomingMessage> {
|
|
402
|
-
return this.commandIncoming.asObservable()
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
406
|
-
// Peer Discovery
|
|
407
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
408
|
-
|
|
409
|
-
/** Enable automatic peer discovery via GetPeerServers query */
|
|
410
|
-
enablePeerDiscovery(enabled: boolean, secure = false): void {
|
|
411
|
-
this.peerDiscoveryEnabled = enabled
|
|
412
|
-
this.useSecureWebSocket = secure
|
|
413
|
-
|
|
414
|
-
if (!enabled && this.peerDiscoverySubscription) {
|
|
415
|
-
this.peerDiscoverySubscription.unsubscribe()
|
|
416
|
-
this.peerDiscoverySubscription = null
|
|
417
|
-
} else if (enabled && this.hasOpenConnection()) {
|
|
418
|
-
this.startPeerDiscovery()
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
private startPeerDiscovery(): void {
|
|
423
|
-
this.peerDiscoverySubscription?.unsubscribe()
|
|
424
|
-
this.logConnection('peer_discovery_started', {
|
|
425
|
-
secure: this.useSecureWebSocket,
|
|
426
|
-
via: 'query:GetPeerServers',
|
|
427
|
-
})
|
|
428
|
-
|
|
429
|
-
this.peerDiscoverySubscription = this.watchQuery(
|
|
430
|
-
new GetPeerServers({}),
|
|
431
|
-
).subscribe((servers: Server[]) => {
|
|
432
|
-
const addresses = servers.map((s) =>
|
|
433
|
-
this.useSecureWebSocket
|
|
434
|
-
? `wss://${s.address}/myko`
|
|
435
|
-
: `ws://${s.address}:${s.port}/myko`,
|
|
436
|
-
)
|
|
437
|
-
this.logConnection('peer_discovery_update', {
|
|
438
|
-
peers: servers.length,
|
|
439
|
-
addresses,
|
|
440
|
-
})
|
|
441
|
-
// Discovered peers are ephemeral: if they disconnect, wait for discovery
|
|
442
|
-
// to advertise them again rather than actively redialing.
|
|
443
|
-
if (addresses.length > 0) this.addServers(addresses, false)
|
|
444
|
-
})
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
private stopPeerDiscovery(): void {
|
|
448
|
-
this.peerDiscoverySubscription?.unsubscribe()
|
|
449
|
-
this.peerDiscoverySubscription = null
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
453
|
-
// Auth & Stats
|
|
454
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
455
|
-
|
|
456
|
-
/** Set authentication token for commands */
|
|
457
|
-
setToken(token: string | null): void {
|
|
458
|
-
this.userToken = token
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/** Observable of all errors */
|
|
462
|
-
get errors$(): Observable<MykoError> {
|
|
463
|
-
const toError = <
|
|
464
|
-
T extends {
|
|
465
|
-
event: MykoErrorEvent
|
|
466
|
-
data: { tx: string; message: string }
|
|
467
|
-
},
|
|
468
|
-
>(
|
|
469
|
-
e: T,
|
|
470
|
-
): MykoError => ({ event: e.event, tx: e.data.tx, message: e.data.message })
|
|
471
|
-
|
|
472
|
-
return merge(
|
|
473
|
-
this.queryErrors.pipe(map(toError)),
|
|
474
|
-
this.commandErrors.pipe(map(toError)),
|
|
475
|
-
this.reportErrors.pipe(map(toError)),
|
|
476
|
-
)
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/** Observable of successful command completions (tx id) */
|
|
480
|
-
get successes$(): Observable<string> {
|
|
481
|
-
return this.commandResponses.pipe(map((r) => r.data.tx))
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
/** Measure round-trip latency */
|
|
485
|
-
async ping(): Promise<number> {
|
|
486
|
-
const id = uuid()
|
|
487
|
-
const nowMs = Date.now()
|
|
488
|
-
// IMPORTANT: the Rust server expects `timestamp: i64`.
|
|
489
|
-
// - JSON cannot encode bigint, so use number in JSON mode.
|
|
490
|
-
// - msgpack can encode bigint as int64, so use bigint in MSGPACK mode.
|
|
491
|
-
const timestamp =
|
|
492
|
-
this.protocol === MykoProtocol.MSGPACK ? BigInt(nowMs) : nowMs
|
|
493
|
-
|
|
494
|
-
this.send({
|
|
495
|
-
event: MykoEvent.Ping,
|
|
496
|
-
data: { id, timestamp } as unknown as PingData,
|
|
497
|
-
} as MykoMessage)
|
|
498
|
-
|
|
499
|
-
return firstValueFrom(
|
|
500
|
-
this.pingResponses.pipe(
|
|
501
|
-
filter((p) => p.data.id === id),
|
|
502
|
-
map((p) => Date.now() - Number(p.data.timestamp)),
|
|
503
|
-
),
|
|
504
|
-
)
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/** Get real-time client statistics (emits every second) */
|
|
508
|
-
stats(): Observable<ClientStats> {
|
|
509
|
-
const pingLatency = interval(1000).pipe(
|
|
510
|
-
switchMap(() => this.ping()),
|
|
511
|
-
catchError(() => of(0)),
|
|
512
|
-
)
|
|
513
|
-
const mpsDown = this.downMsgCounter.pipe(
|
|
514
|
-
bufferTime(100),
|
|
515
|
-
bufferCount(10),
|
|
516
|
-
map((b) => b.flat().length),
|
|
517
|
-
)
|
|
518
|
-
const mpsUp = this.upMsgCounter.pipe(
|
|
519
|
-
bufferTime(100),
|
|
520
|
-
bufferCount(10),
|
|
521
|
-
map((b) => b.flat().length),
|
|
522
|
-
)
|
|
523
|
-
return combineLatest([pingLatency, mpsDown, mpsUp]).pipe(
|
|
524
|
-
map(([ping, down, up]) => ({ ping, mpsDown: down, mpsUp: up })),
|
|
525
|
-
)
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
529
|
-
// Queries & Reports
|
|
530
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
531
|
-
|
|
532
|
-
/** Start a query subscription, returns [tx, responses$] */
|
|
533
|
-
private startQuery<Q extends Query<unknown>>(
|
|
534
|
-
query: Q,
|
|
535
|
-
options?: QueryWatchOptions,
|
|
536
|
-
): [string, Observable<QueryResponseMessage>] {
|
|
537
|
-
if (!query.queryId || !query.queryItemType || !(query as { query?: unknown }).query) {
|
|
538
|
-
const details = {
|
|
539
|
-
ctor: (query as { constructor?: { name?: string } }).constructor?.name ?? 'unknown',
|
|
540
|
-
keys: Object.keys((query as Record<string, unknown>) ?? {}),
|
|
541
|
-
queryId: (query as { queryId?: unknown }).queryId,
|
|
542
|
-
queryItemType: (query as { queryItemType?: unknown }).queryItemType,
|
|
543
|
-
}
|
|
544
|
-
this.logConnection('query_shape_invalid', details)
|
|
545
|
-
throw new Error(
|
|
546
|
-
`Invalid query shape for ${details.ctor}: expected { queryId, queryItemType, query }`,
|
|
547
|
-
)
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const tx = uuid()
|
|
551
|
-
const window = options?.window ?? undefined
|
|
552
|
-
const queryName =
|
|
553
|
-
(query as { constructor?: { name?: string } }).constructor?.name ?? query.queryId
|
|
554
|
-
const wrappedQuery = {
|
|
555
|
-
query: { ...query.query, tx, createdAt: new Date().toISOString() },
|
|
556
|
-
queryId: query.queryId,
|
|
557
|
-
queryItemType: query.queryItemType,
|
|
558
|
-
...(window ? { window } : {}),
|
|
559
|
-
} as WrappedQuery
|
|
560
|
-
|
|
561
|
-
this.activeQueries.set(tx, wrappedQuery)
|
|
562
|
-
this.activeQueryNames.set(tx, queryName)
|
|
563
|
-
this.subscriptionStartMs.set(tx, this.nowMs())
|
|
564
|
-
this.firstResponseLogged.delete(tx)
|
|
565
|
-
this.logConnection('query_subscribe', {
|
|
566
|
-
tx,
|
|
567
|
-
queryId: wrappedQuery.queryId,
|
|
568
|
-
queryItemType: wrappedQuery.queryItemType,
|
|
569
|
-
queryName,
|
|
570
|
-
window: window ?? null,
|
|
571
|
-
activeQueries: this.activeQueries.size,
|
|
572
|
-
})
|
|
573
|
-
this.send({ event: MykoEvent.Query, data: wrappedQuery })
|
|
574
|
-
|
|
575
|
-
const responses$ = new Observable<QueryResponseMessage>((subscriber) => {
|
|
576
|
-
const responseSub = this.queryResponses
|
|
577
|
-
.pipe(filter((r) => r.data.tx === tx))
|
|
578
|
-
.subscribe({
|
|
579
|
-
next: (response) => subscriber.next(response),
|
|
580
|
-
error: (error) => subscriber.error(error),
|
|
581
|
-
})
|
|
582
|
-
|
|
583
|
-
const errorSub = this.queryErrors
|
|
584
|
-
.pipe(filter((error) => error.data.tx === tx))
|
|
585
|
-
.subscribe((error) => {
|
|
586
|
-
subscriber.error(new Error(error.data.message))
|
|
587
|
-
})
|
|
588
|
-
|
|
589
|
-
return () => {
|
|
590
|
-
responseSub.unsubscribe()
|
|
591
|
-
errorSub.unsubscribe()
|
|
592
|
-
}
|
|
593
|
-
}).pipe(
|
|
594
|
-
finalize(() => {
|
|
595
|
-
this.logConnection('query_cancel', {
|
|
596
|
-
tx,
|
|
597
|
-
queryId: wrappedQuery.queryId,
|
|
598
|
-
queryItemType: wrappedQuery.queryItemType,
|
|
599
|
-
queryName,
|
|
600
|
-
activeQueriesBefore: this.activeQueries.size,
|
|
601
|
-
})
|
|
602
|
-
this.activeQueries.delete(tx)
|
|
603
|
-
this.activeQueryNames.delete(tx)
|
|
604
|
-
this.subscriptionStartMs.delete(tx)
|
|
605
|
-
this.firstResponseLogged.delete(tx)
|
|
606
|
-
this.send({ event: MykoEvent.QueryCancel, data: { tx } })
|
|
607
|
-
}),
|
|
608
|
-
)
|
|
609
|
-
|
|
610
|
-
return [tx, responses$]
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/** Update server-side window for an active query subscription */
|
|
614
|
-
setQueryWindow(tx: string, window: QueryWindow | null): void {
|
|
615
|
-
const active = this.activeQueries.get(tx)
|
|
616
|
-
if (!active) return
|
|
617
|
-
|
|
618
|
-
const updated = {
|
|
619
|
-
...active,
|
|
620
|
-
...(window ? { window } : {}),
|
|
621
|
-
} as WrappedQuery
|
|
622
|
-
if (!window) {
|
|
623
|
-
delete (updated as { window?: QueryWindow }).window
|
|
624
|
-
}
|
|
625
|
-
this.activeQueries.set(tx, updated)
|
|
626
|
-
this.logConnection('query_window_set', {
|
|
627
|
-
tx,
|
|
628
|
-
queryId: active.queryId,
|
|
629
|
-
queryItemType: active.queryItemType,
|
|
630
|
-
queryName: this.activeQueryNames.get(tx) ?? active.queryId,
|
|
631
|
-
window,
|
|
632
|
-
})
|
|
633
|
-
|
|
634
|
-
this.send({ event: MykoEvent.QueryWindow, data: { tx, window } as unknown } as MykoMessage)
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/** Start a view subscription, returns [tx, responses$] */
|
|
638
|
-
private startView<V extends View<unknown>>(
|
|
639
|
-
view: V,
|
|
640
|
-
options?: QueryWatchOptions,
|
|
641
|
-
): [string, Observable<QueryResponseMessage>] {
|
|
642
|
-
if (!view.viewId || !view.viewItemType || !(view as { view?: unknown }).view) {
|
|
643
|
-
const details = {
|
|
644
|
-
ctor: (view as { constructor?: { name?: string } }).constructor?.name ?? 'unknown',
|
|
645
|
-
keys: Object.keys((view as Record<string, unknown>) ?? {}),
|
|
646
|
-
viewId: (view as { viewId?: unknown }).viewId,
|
|
647
|
-
viewItemType: (view as { viewItemType?: unknown }).viewItemType,
|
|
648
|
-
}
|
|
649
|
-
this.logConnection('view_shape_invalid', details)
|
|
650
|
-
throw new Error(
|
|
651
|
-
`Invalid view shape for ${details.ctor}: expected { viewId, viewItemType, view }`,
|
|
652
|
-
)
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
const tx = uuid()
|
|
656
|
-
const window = options?.window ?? undefined
|
|
657
|
-
const viewName =
|
|
658
|
-
(view as { constructor?: { name?: string } }).constructor?.name ?? view.viewId
|
|
659
|
-
const wrappedView = {
|
|
660
|
-
view: { ...view.view, tx, createdAt: new Date().toISOString() },
|
|
661
|
-
viewId: view.viewId,
|
|
662
|
-
viewItemType: view.viewItemType,
|
|
663
|
-
...(window ? { window } : {}),
|
|
664
|
-
} as WrappedView
|
|
665
|
-
|
|
666
|
-
this.activeViews.set(tx, wrappedView)
|
|
667
|
-
this.activeViewNames.set(tx, viewName)
|
|
668
|
-
this.subscriptionStartMs.set(tx, this.nowMs())
|
|
669
|
-
this.firstResponseLogged.delete(tx)
|
|
670
|
-
this.logConnection('view_subscribe', {
|
|
671
|
-
tx,
|
|
672
|
-
viewId: wrappedView.viewId,
|
|
673
|
-
viewItemType: wrappedView.viewItemType,
|
|
674
|
-
viewName,
|
|
675
|
-
window: wrappedView.window ?? null,
|
|
676
|
-
activeViews: this.activeViews.size,
|
|
677
|
-
})
|
|
678
|
-
this.send({ event: MykoEvent.View, data: wrappedView })
|
|
679
|
-
|
|
680
|
-
const responses$ = new Observable<QueryResponseMessage>((subscriber) => {
|
|
681
|
-
const responseSub = this.queryResponses
|
|
682
|
-
.pipe(filter((r) => r.data.tx === tx))
|
|
683
|
-
.subscribe({
|
|
684
|
-
next: (response) => subscriber.next(response),
|
|
685
|
-
error: (error) => subscriber.error(error),
|
|
686
|
-
})
|
|
687
|
-
|
|
688
|
-
const errorSub = this.queryErrors
|
|
689
|
-
.pipe(filter((error) => error.data.tx === tx))
|
|
690
|
-
.subscribe((error) => {
|
|
691
|
-
subscriber.error(new Error(error.data.message))
|
|
692
|
-
})
|
|
693
|
-
|
|
694
|
-
return () => {
|
|
695
|
-
responseSub.unsubscribe()
|
|
696
|
-
errorSub.unsubscribe()
|
|
697
|
-
}
|
|
698
|
-
}).pipe(
|
|
699
|
-
finalize(() => {
|
|
700
|
-
this.logConnection('view_cancel', {
|
|
701
|
-
tx,
|
|
702
|
-
viewId: wrappedView.viewId,
|
|
703
|
-
viewName,
|
|
704
|
-
activeViewsBefore: this.activeViews.size,
|
|
705
|
-
})
|
|
706
|
-
this.activeViews.delete(tx)
|
|
707
|
-
this.activeViewNames.delete(tx)
|
|
708
|
-
this.subscriptionStartMs.delete(tx)
|
|
709
|
-
this.firstResponseLogged.delete(tx)
|
|
710
|
-
this.send({ event: MykoEvent.ViewCancel, data: { tx } })
|
|
711
|
-
}),
|
|
712
|
-
)
|
|
713
|
-
|
|
714
|
-
return [tx, responses$]
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/** Update server-side window for an active view subscription */
|
|
718
|
-
setViewWindow(tx: string, window: QueryWindow | null): void {
|
|
719
|
-
const active = this.activeViews.get(tx)
|
|
720
|
-
if (!active) return
|
|
721
|
-
|
|
722
|
-
const updated = {
|
|
723
|
-
...active,
|
|
724
|
-
...(window ? { window } : {}),
|
|
725
|
-
} as WrappedView
|
|
726
|
-
if (!window) {
|
|
727
|
-
delete (updated as { window?: QueryWindow }).window
|
|
728
|
-
}
|
|
729
|
-
this.activeViews.set(tx, updated)
|
|
730
|
-
this.logConnection('view_window_set', {
|
|
731
|
-
tx,
|
|
732
|
-
viewId: active.viewId,
|
|
733
|
-
viewName: this.activeViewNames.get(tx) ?? active.viewId,
|
|
734
|
-
window,
|
|
735
|
-
})
|
|
736
|
-
|
|
737
|
-
this.send({ event: MykoEvent.ViewWindow, data: { tx, window } as unknown } as MykoMessage)
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
/** Watch a query and receive live updates with automatic deduplication */
|
|
741
|
-
watchQuery<Q extends Query<unknown>>(
|
|
742
|
-
query: Q,
|
|
743
|
-
options?: QueryWatchOptions,
|
|
744
|
-
): Observable<QueryResult<Q>> {
|
|
745
|
-
const cacheKey = queryCacheKey(query, options)
|
|
746
|
-
|
|
747
|
-
const existing = this.sharedQueries.get(cacheKey)
|
|
748
|
-
if (existing) return existing as Observable<QueryResult<Q>>
|
|
749
|
-
|
|
750
|
-
const [, responses$] = this.startQuery(query, options)
|
|
751
|
-
|
|
752
|
-
const shared$ = responses$.pipe(
|
|
753
|
-
scan((acc, update) => {
|
|
754
|
-
if (BigInt(update.data.sequence) === 0n) acc.clear()
|
|
755
|
-
for (const id of update.data.deletes) acc.delete(id)
|
|
756
|
-
for (const wrapped of update.data.upserts) {
|
|
757
|
-
const item = wrapped.item as { id: string }
|
|
758
|
-
if (item?.id) acc.set(item.id, wrapped)
|
|
759
|
-
}
|
|
760
|
-
return acc
|
|
761
|
-
}, new Map<string, WrappedItem>()),
|
|
762
|
-
map((items) => [...items.values()].map((w) => w.item) as QueryResult<Q>),
|
|
763
|
-
finalize(() => {
|
|
764
|
-
this.sharedQueries.delete(cacheKey)
|
|
765
|
-
}),
|
|
766
|
-
shareReplay({ bufferSize: 1, refCount: true }),
|
|
767
|
-
// Defensive copy: shareReplay replays the same array reference to all
|
|
768
|
-
// subscribers, so a mutation (e.g. .shift()) by one subscriber would
|
|
769
|
-
// corrupt the shared value for others. Cloning per-subscriber prevents this.
|
|
770
|
-
map((x) => (Array.isArray(x) ? (x.slice() as QueryResult<Q>) : x)),
|
|
771
|
-
)
|
|
772
|
-
|
|
773
|
-
this.sharedQueries.set(cacheKey, shared$)
|
|
774
|
-
return shared$
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
/** Watch a view and receive live updates with automatic deduplication */
|
|
778
|
-
watchView<V extends View<unknown>>(
|
|
779
|
-
view: V,
|
|
780
|
-
options?: QueryWatchOptions,
|
|
781
|
-
): Observable<ViewResult<V>> {
|
|
782
|
-
const cacheKey = viewCacheKey(view, options)
|
|
783
|
-
|
|
784
|
-
const existing = this.sharedViews.get(cacheKey)
|
|
785
|
-
if (existing) return existing as Observable<ViewResult<V>>
|
|
786
|
-
|
|
787
|
-
const [, responses$] = this.startView(view, options)
|
|
788
|
-
|
|
789
|
-
const shared$ = responses$.pipe(
|
|
790
|
-
scan((acc, update) => {
|
|
791
|
-
if (BigInt(update.data.sequence) === 0n) acc.clear()
|
|
792
|
-
for (const id of update.data.deletes) acc.delete(id)
|
|
793
|
-
for (const wrapped of update.data.upserts) {
|
|
794
|
-
const item = wrapped.item as { id: string }
|
|
795
|
-
if (item?.id) acc.set(item.id, wrapped)
|
|
796
|
-
}
|
|
797
|
-
return acc
|
|
798
|
-
}, new Map<string, WrappedItem>()),
|
|
799
|
-
map((items) => [...items.values()].map((w) => w.item) as ViewResult<V>),
|
|
800
|
-
finalize(() => {
|
|
801
|
-
this.sharedViews.delete(cacheKey)
|
|
802
|
-
}),
|
|
803
|
-
shareReplay({ bufferSize: 1, refCount: true }),
|
|
804
|
-
map((x) => (Array.isArray(x) ? (x.slice() as ViewResult<V>) : x)),
|
|
805
|
-
)
|
|
806
|
-
|
|
807
|
-
this.sharedViews.set(cacheKey, shared$)
|
|
808
|
-
return shared$
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
/** Watch a query and receive raw diff events */
|
|
812
|
-
watchQueryDiff<Q extends Query<unknown>>(
|
|
813
|
-
query: Q,
|
|
814
|
-
options?: QueryWatchOptions,
|
|
815
|
-
): Observable<QueryDiff<QueryItem<Q>>> {
|
|
816
|
-
const cacheKey = queryCacheKey(query, options)
|
|
817
|
-
const existing = this.sharedQueryDiffs.get(cacheKey)
|
|
818
|
-
if (existing) return existing as Observable<QueryDiff<QueryItem<Q>>>
|
|
819
|
-
|
|
820
|
-
const [, responses$] = this.startQuery(query, options)
|
|
821
|
-
const shared$ = responses$.pipe(
|
|
822
|
-
map((r) => ({
|
|
823
|
-
sequence: BigInt(r.data.sequence),
|
|
824
|
-
deletes: r.data.deletes.slice(),
|
|
825
|
-
upserts: r.data.upserts.map(
|
|
826
|
-
(w: WrappedItem) => w.item,
|
|
827
|
-
) as QueryItem<Q>[],
|
|
828
|
-
})),
|
|
829
|
-
finalize(() => {
|
|
830
|
-
this.sharedQueryDiffs.delete(cacheKey)
|
|
831
|
-
}),
|
|
832
|
-
shareReplay({ bufferSize: 1, refCount: true }),
|
|
833
|
-
map((diff) => ({
|
|
834
|
-
sequence: diff.sequence,
|
|
835
|
-
deletes: diff.deletes.slice(),
|
|
836
|
-
upserts: diff.upserts.slice(),
|
|
837
|
-
})),
|
|
838
|
-
)
|
|
839
|
-
this.sharedQueryDiffs.set(cacheKey, shared$)
|
|
840
|
-
return shared$
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
/** Watch a view and receive raw diff events */
|
|
844
|
-
watchViewDiff<V extends View<unknown>>(
|
|
845
|
-
view: V,
|
|
846
|
-
options?: QueryWatchOptions,
|
|
847
|
-
): Observable<QueryDiff<ViewItem<V>>> {
|
|
848
|
-
const cacheKey = viewCacheKey(view, options)
|
|
849
|
-
const existing = this.sharedViewDiffs.get(cacheKey)
|
|
850
|
-
if (existing) return existing as Observable<QueryDiff<ViewItem<V>>>
|
|
851
|
-
|
|
852
|
-
const [, responses$] = this.startView(view, options)
|
|
853
|
-
const shared$ = responses$.pipe(
|
|
854
|
-
map((r) => ({
|
|
855
|
-
sequence: BigInt(r.data.sequence),
|
|
856
|
-
deletes: r.data.deletes.slice(),
|
|
857
|
-
upserts: r.data.upserts.map(
|
|
858
|
-
(w: WrappedItem) => w.item,
|
|
859
|
-
) as ViewItem<V>[],
|
|
860
|
-
})),
|
|
861
|
-
finalize(() => {
|
|
862
|
-
this.sharedViewDiffs.delete(cacheKey)
|
|
863
|
-
}),
|
|
864
|
-
shareReplay({ bufferSize: 1, refCount: true }),
|
|
865
|
-
map((diff) => ({
|
|
866
|
-
sequence: diff.sequence,
|
|
867
|
-
deletes: diff.deletes.slice(),
|
|
868
|
-
upserts: diff.upserts.slice(),
|
|
869
|
-
})),
|
|
870
|
-
)
|
|
871
|
-
this.sharedViewDiffs.set(cacheKey, shared$)
|
|
872
|
-
return shared$
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
/**
|
|
876
|
-
* Start a live query with a mutable server-side window.
|
|
877
|
-
* Use `setWindow` to scroll without re-subscribing.
|
|
878
|
-
*/
|
|
879
|
-
watchQueryWindowed<Q extends Query<unknown>>(
|
|
880
|
-
query: Q,
|
|
881
|
-
options?: QueryWatchOptions,
|
|
882
|
-
): {
|
|
883
|
-
tx: string
|
|
884
|
-
results$: Observable<QueryResult<Q>>
|
|
885
|
-
windowInfo$: Observable<QueryWindowInfo>
|
|
886
|
-
setWindow: (window: QueryWindow | null) => void
|
|
887
|
-
} {
|
|
888
|
-
const [tx, responses$] = this.startQuery(query, options)
|
|
889
|
-
const sharedResponses$ = responses$.pipe(
|
|
890
|
-
shareReplay({ bufferSize: 1, refCount: true }),
|
|
891
|
-
)
|
|
892
|
-
const results$ = sharedResponses$.pipe(
|
|
893
|
-
scan((state, update) => {
|
|
894
|
-
const data = update.data as QueryResponseMessage['data'] & {
|
|
895
|
-
total_count?: number
|
|
896
|
-
totalCount?: number
|
|
897
|
-
window?: QueryWindow | null
|
|
898
|
-
changes?: Array<
|
|
899
|
-
| { kind: 'upsert'; item: WrappedItem }
|
|
900
|
-
| { kind: 'delete'; id: string }
|
|
901
|
-
| {
|
|
902
|
-
kind: 'windowOrder'
|
|
903
|
-
ids: string[]
|
|
904
|
-
totalCount?: number
|
|
905
|
-
total_count?: number
|
|
906
|
-
window?: QueryWindow | null
|
|
907
|
-
}
|
|
908
|
-
>
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
if (BigInt(update.data.sequence) === 0n) {
|
|
912
|
-
state.cache.clear()
|
|
913
|
-
state.visibleIds = []
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
for (const id of update.data.deletes) state.cache.delete(id)
|
|
917
|
-
for (const wrapped of update.data.upserts) {
|
|
918
|
-
const item = wrapped.item as { id?: string }
|
|
919
|
-
if (item?.id) state.cache.set(item.id, wrapped)
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
const order = data.changes?.find(
|
|
923
|
-
(change: NonNullable<typeof data.changes>[number]): change is Extract<NonNullable<typeof data.changes>[number], { kind: 'windowOrder' }> =>
|
|
924
|
-
change.kind === 'windowOrder',
|
|
925
|
-
)
|
|
926
|
-
if (order) {
|
|
927
|
-
state.visibleIds = order.ids.slice()
|
|
928
|
-
} else {
|
|
929
|
-
// Fallback when window-order diffs are unavailable: derive visible ids
|
|
930
|
-
// from current cache contents (in insertion order).
|
|
931
|
-
state.visibleIds = [...state.cache.keys()]
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
return state
|
|
935
|
-
}, {
|
|
936
|
-
cache: new Map<string, WrappedItem>(),
|
|
937
|
-
visibleIds: [] as string[],
|
|
938
|
-
}),
|
|
939
|
-
map((state) =>
|
|
940
|
-
state.visibleIds
|
|
941
|
-
.map((id) => state.cache.get(id)?.item)
|
|
942
|
-
.filter((item): item is JsonValue => item !== undefined) as QueryResult<Q>,
|
|
943
|
-
),
|
|
944
|
-
map((x) => x.slice() as QueryResult<Q>),
|
|
945
|
-
)
|
|
946
|
-
const windowInfo$ = sharedResponses$.pipe(
|
|
947
|
-
map((update) => {
|
|
948
|
-
const data = update.data as QueryResponseMessage['data'] & {
|
|
949
|
-
total_count?: number
|
|
950
|
-
totalCount?: number
|
|
951
|
-
window?: QueryWindow | null
|
|
952
|
-
changes?: Array<
|
|
953
|
-
| { kind: 'upsert'; item: WrappedItem }
|
|
954
|
-
| { kind: 'delete'; id: string }
|
|
955
|
-
| {
|
|
956
|
-
kind: 'windowOrder'
|
|
957
|
-
ids: string[]
|
|
958
|
-
totalCount?: number
|
|
959
|
-
total_count?: number
|
|
960
|
-
window?: QueryWindow | null
|
|
961
|
-
}
|
|
962
|
-
>
|
|
963
|
-
}
|
|
964
|
-
const order = data.changes?.find(
|
|
965
|
-
(change: NonNullable<typeof data.changes>[number]): change is Extract<NonNullable<typeof data.changes>[number], { kind: 'windowOrder' }> =>
|
|
966
|
-
change.kind === 'windowOrder',
|
|
967
|
-
)
|
|
968
|
-
const orderTotalCount = order?.totalCount ?? order?.total_count
|
|
969
|
-
return {
|
|
970
|
-
totalCount:
|
|
971
|
-
typeof data.totalCount === 'number'
|
|
972
|
-
? data.totalCount
|
|
973
|
-
: typeof data.total_count === 'number'
|
|
974
|
-
? data.total_count
|
|
975
|
-
: typeof orderTotalCount === 'number'
|
|
976
|
-
? orderTotalCount
|
|
977
|
-
: null,
|
|
978
|
-
window: data.window ?? order?.window ?? null,
|
|
979
|
-
} satisfies QueryWindowInfo
|
|
980
|
-
}),
|
|
981
|
-
shareReplay({ bufferSize: 1, refCount: true }),
|
|
982
|
-
)
|
|
983
|
-
|
|
984
|
-
return {
|
|
985
|
-
tx,
|
|
986
|
-
results$,
|
|
987
|
-
windowInfo$,
|
|
988
|
-
setWindow: (window) => this.setQueryWindow(tx, window),
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
/** Start a live view with a mutable server-side window. */
|
|
993
|
-
watchViewWindowed<V extends View<unknown>>(
|
|
994
|
-
view: V,
|
|
995
|
-
options?: QueryWatchOptions,
|
|
996
|
-
): {
|
|
997
|
-
tx: string
|
|
998
|
-
results$: Observable<ViewResult<V>>
|
|
999
|
-
windowInfo$: Observable<QueryWindowInfo>
|
|
1000
|
-
setWindow: (window: QueryWindow | null) => void
|
|
1001
|
-
} {
|
|
1002
|
-
const [tx, responses$] = this.startView(view, options)
|
|
1003
|
-
const sharedResponses$ = responses$.pipe(
|
|
1004
|
-
shareReplay({ bufferSize: 1, refCount: true }),
|
|
1005
|
-
)
|
|
1006
|
-
const results$ = sharedResponses$.pipe(
|
|
1007
|
-
scan((state, update) => {
|
|
1008
|
-
const data = update.data as QueryResponseMessage['data'] & {
|
|
1009
|
-
total_count?: number
|
|
1010
|
-
totalCount?: number
|
|
1011
|
-
window?: QueryWindow | null
|
|
1012
|
-
changes?: Array<
|
|
1013
|
-
| { kind: 'upsert'; item: WrappedItem }
|
|
1014
|
-
| { kind: 'delete'; id: string }
|
|
1015
|
-
| {
|
|
1016
|
-
kind: 'windowOrder'
|
|
1017
|
-
ids: string[]
|
|
1018
|
-
totalCount?: number
|
|
1019
|
-
total_count?: number
|
|
1020
|
-
window?: QueryWindow | null
|
|
1021
|
-
}
|
|
1022
|
-
>
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
if (BigInt(update.data.sequence) === 0n) {
|
|
1026
|
-
state.cache.clear()
|
|
1027
|
-
state.visibleIds = []
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
for (const id of update.data.deletes) state.cache.delete(id)
|
|
1031
|
-
for (const wrapped of update.data.upserts) {
|
|
1032
|
-
const item = wrapped.item as { id?: string }
|
|
1033
|
-
if (item?.id) state.cache.set(item.id, wrapped)
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
const order = data.changes?.find(
|
|
1037
|
-
(change: NonNullable<typeof data.changes>[number]): change is Extract<NonNullable<typeof data.changes>[number], { kind: 'windowOrder' }> =>
|
|
1038
|
-
change.kind === 'windowOrder',
|
|
1039
|
-
)
|
|
1040
|
-
if (order) {
|
|
1041
|
-
state.visibleIds = order.ids.slice()
|
|
1042
|
-
} else {
|
|
1043
|
-
state.visibleIds = [...state.cache.keys()]
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
return state
|
|
1047
|
-
}, {
|
|
1048
|
-
cache: new Map<string, WrappedItem>(),
|
|
1049
|
-
visibleIds: [] as string[],
|
|
1050
|
-
}),
|
|
1051
|
-
map((state) =>
|
|
1052
|
-
state.visibleIds
|
|
1053
|
-
.map((id) => state.cache.get(id)?.item)
|
|
1054
|
-
.filter((item): item is JsonValue => item !== undefined) as ViewResult<V>,
|
|
1055
|
-
),
|
|
1056
|
-
map((x) => x.slice() as ViewResult<V>),
|
|
1057
|
-
)
|
|
1058
|
-
const windowInfo$ = sharedResponses$.pipe(
|
|
1059
|
-
map((update) => {
|
|
1060
|
-
const data = update.data as QueryResponseMessage['data'] & {
|
|
1061
|
-
total_count?: number
|
|
1062
|
-
totalCount?: number
|
|
1063
|
-
window?: QueryWindow | null
|
|
1064
|
-
changes?: Array<
|
|
1065
|
-
| { kind: 'upsert'; item: WrappedItem }
|
|
1066
|
-
| { kind: 'delete'; id: string }
|
|
1067
|
-
| {
|
|
1068
|
-
kind: 'windowOrder'
|
|
1069
|
-
ids: string[]
|
|
1070
|
-
totalCount?: number
|
|
1071
|
-
total_count?: number
|
|
1072
|
-
window?: QueryWindow | null
|
|
1073
|
-
}
|
|
1074
|
-
>
|
|
1075
|
-
}
|
|
1076
|
-
const order = data.changes?.find(
|
|
1077
|
-
(change: NonNullable<typeof data.changes>[number]): change is Extract<NonNullable<typeof data.changes>[number], { kind: 'windowOrder' }> =>
|
|
1078
|
-
change.kind === 'windowOrder',
|
|
1079
|
-
)
|
|
1080
|
-
const orderTotalCount = order?.totalCount ?? order?.total_count
|
|
1081
|
-
return {
|
|
1082
|
-
totalCount:
|
|
1083
|
-
typeof data.totalCount === 'number'
|
|
1084
|
-
? data.totalCount
|
|
1085
|
-
: typeof data.total_count === 'number'
|
|
1086
|
-
? data.total_count
|
|
1087
|
-
: typeof orderTotalCount === 'number'
|
|
1088
|
-
? orderTotalCount
|
|
1089
|
-
: null,
|
|
1090
|
-
window: data.window ?? order?.window ?? null,
|
|
1091
|
-
} satisfies QueryWindowInfo
|
|
1092
|
-
}),
|
|
1093
|
-
shareReplay({ bufferSize: 1, refCount: true }),
|
|
1094
|
-
)
|
|
1095
|
-
return {
|
|
1096
|
-
tx,
|
|
1097
|
-
results$,
|
|
1098
|
-
windowInfo$,
|
|
1099
|
-
setWindow: (window) => this.setViewWindow(tx, window),
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
/** Watch a report with automatic deduplication */
|
|
1104
|
-
watchReport<R extends Report<unknown>>(
|
|
1105
|
-
report: R,
|
|
1106
|
-
): Observable<ReportResult<R>> {
|
|
1107
|
-
const cacheKey = reportCacheKey(report)
|
|
1108
|
-
|
|
1109
|
-
const existing = this.sharedReports.get(cacheKey)
|
|
1110
|
-
if (existing) return existing as Observable<ReportResult<R>>
|
|
1111
|
-
|
|
1112
|
-
const tx = uuid()
|
|
1113
|
-
const reportName =
|
|
1114
|
-
(report as { constructor?: { name?: string } }).constructor?.name ??
|
|
1115
|
-
report.reportId
|
|
1116
|
-
const wrappedReport: WrappedReport = {
|
|
1117
|
-
report: { ...report.report, tx },
|
|
1118
|
-
reportId: report.reportId,
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
this.activeReports.set(tx, wrappedReport)
|
|
1122
|
-
this.activeReportNames.set(tx, reportName)
|
|
1123
|
-
this.logConnection('report_subscribe', {
|
|
1124
|
-
tx,
|
|
1125
|
-
reportId: wrappedReport.reportId,
|
|
1126
|
-
reportName,
|
|
1127
|
-
report: report.report,
|
|
1128
|
-
activeReports: this.activeReports.size,
|
|
1129
|
-
})
|
|
1130
|
-
this.send({ event: MykoEvent.Report, data: wrappedReport })
|
|
1131
|
-
|
|
1132
|
-
const shared$ = this.reportResponses.pipe(
|
|
1133
|
-
filter((r) => r.data.tx === tx),
|
|
1134
|
-
map((r) => {
|
|
1135
|
-
this.logConnection('report_response', {
|
|
1136
|
-
tx,
|
|
1137
|
-
reportId: wrappedReport.reportId,
|
|
1138
|
-
reportName,
|
|
1139
|
-
})
|
|
1140
|
-
return r
|
|
1141
|
-
}),
|
|
1142
|
-
map((r) => r.data.response as ReportResult<R>),
|
|
1143
|
-
finalize(() => {
|
|
1144
|
-
this.logConnection('report_cancel', {
|
|
1145
|
-
tx,
|
|
1146
|
-
reportId: wrappedReport.reportId,
|
|
1147
|
-
reportName,
|
|
1148
|
-
activeReportsBefore: this.activeReports.size,
|
|
1149
|
-
})
|
|
1150
|
-
this.sharedReports.delete(cacheKey)
|
|
1151
|
-
this.activeReports.delete(tx)
|
|
1152
|
-
this.activeReportNames.delete(tx)
|
|
1153
|
-
this.send({ event: MykoEvent.ReportCancel, data: { tx } })
|
|
1154
|
-
}),
|
|
1155
|
-
shareReplay({ bufferSize: 1, refCount: true }),
|
|
1156
|
-
// Defensive copy: prevent one subscriber's mutations from affecting others
|
|
1157
|
-
map((x) => {
|
|
1158
|
-
if (Array.isArray(x)) return x.slice() as ReportResult<R>
|
|
1159
|
-
if (x && typeof x === 'object') {
|
|
1160
|
-
return { ...(x as Record<string, unknown>) } as ReportResult<R>
|
|
1161
|
-
}
|
|
1162
|
-
return x
|
|
1163
|
-
}),
|
|
1164
|
-
)
|
|
1165
|
-
|
|
1166
|
-
this.sharedReports.set(cacheKey, shared$)
|
|
1167
|
-
return shared$
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1171
|
-
// Commands & Events
|
|
1172
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1173
|
-
|
|
1174
|
-
/** Send an event to the server */
|
|
1175
|
-
sendEvent(event: MEvent): void {
|
|
1176
|
-
// Pulses are latency-sensitive; bypass batching and send immediately.
|
|
1177
|
-
if (event.itemType === 'Pulse') {
|
|
1178
|
-
this.flushPendingEventBatch()
|
|
1179
|
-
this.sendNow({ event: MykoEvent.Event, data: event })
|
|
1180
|
-
return
|
|
1181
|
-
}
|
|
1182
|
-
this.sendEventBatch([event])
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
/** Send a batch of events to the server */
|
|
1186
|
-
sendEventBatch(events: MEvent[]): void {
|
|
1187
|
-
if (events.length === 0) return
|
|
1188
|
-
|
|
1189
|
-
const buffered: MEvent[] = []
|
|
1190
|
-
const immediatePulses: MEvent[] = []
|
|
1191
|
-
for (const event of events) {
|
|
1192
|
-
if (event.itemType === 'Pulse') {
|
|
1193
|
-
immediatePulses.push(event)
|
|
1194
|
-
} else {
|
|
1195
|
-
buffered.push(event)
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
if (buffered.length > 0) {
|
|
1200
|
-
this.pendingEventBatch.push(...buffered)
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
if (immediatePulses.length > 0) {
|
|
1204
|
-
this.flushPendingEventBatch()
|
|
1205
|
-
for (const pulse of immediatePulses) {
|
|
1206
|
-
this.sendNow({ event: MykoEvent.Event, data: pulse })
|
|
1207
|
-
}
|
|
1208
|
-
return
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
if (this.pendingEventBatch.length >= this.eventBatchMaxSize) {
|
|
1212
|
-
this.flushPendingEventBatch()
|
|
1213
|
-
return
|
|
1214
|
-
}
|
|
1215
|
-
this.scheduleEventBatchFlush()
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
/** Send a command and wait for response */
|
|
1219
|
-
sendCommand<C extends Command<unknown>>(
|
|
1220
|
-
command: C,
|
|
1221
|
-
): Promise<CommandResult<C>> {
|
|
1222
|
-
const tx = uuid()
|
|
1223
|
-
|
|
1224
|
-
const wrappedCommand = {
|
|
1225
|
-
command: {
|
|
1226
|
-
...command.command,
|
|
1227
|
-
tx,
|
|
1228
|
-
createdAt: new Date().toISOString(),
|
|
1229
|
-
...(this.userToken && { userToken: this.userToken }),
|
|
1230
|
-
},
|
|
1231
|
-
commandId: command.commandId,
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
return new Promise<CommandResult<C>>((resolve, reject) => {
|
|
1235
|
-
const responseSub = this.commandResponses
|
|
1236
|
-
.pipe(filter((r) => r.data.tx === tx))
|
|
1237
|
-
.subscribe((r) => {
|
|
1238
|
-
cleanup()
|
|
1239
|
-
resolve(r.data.response as CommandResult<C>)
|
|
1240
|
-
})
|
|
1241
|
-
|
|
1242
|
-
const errorSub = this.commandErrors
|
|
1243
|
-
.pipe(filter((r) => r.data.tx === tx))
|
|
1244
|
-
.subscribe((r) => {
|
|
1245
|
-
cleanup()
|
|
1246
|
-
reject(new Error(r.data.message))
|
|
1247
|
-
})
|
|
1248
|
-
|
|
1249
|
-
const cleanup = () => {
|
|
1250
|
-
responseSub.unsubscribe()
|
|
1251
|
-
errorSub.unsubscribe()
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
this.send({ event: MykoEvent.Command, data: wrappedCommand })
|
|
1255
|
-
})
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1259
|
-
// Private: Socket Management
|
|
1260
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1261
|
-
|
|
1262
|
-
private getFirstOpenSocket(): ManagedSocket | null {
|
|
1263
|
-
for (const m of this.sockets.values()) {
|
|
1264
|
-
if (m.ws.readyState === WebSocket.OPEN) return m
|
|
1265
|
-
}
|
|
1266
|
-
return null
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
private hasOpenConnection(): boolean {
|
|
1270
|
-
return this.getFirstOpenSocket() !== null
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
private hasConnectionTo(address: string): boolean {
|
|
1274
|
-
return this.endpointSockets.has(this.endpointKey(address))
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
private parseAddress(address: string): { host: string; port: number } | null {
|
|
1278
|
-
try {
|
|
1279
|
-
const url = new URL(address)
|
|
1280
|
-
const port = url.port
|
|
1281
|
-
? parseInt(url.port, 10)
|
|
1282
|
-
: url.protocol === 'wss:'
|
|
1283
|
-
? 443
|
|
1284
|
-
: 80
|
|
1285
|
-
return { host: url.hostname.toLowerCase(), port }
|
|
1286
|
-
} catch {
|
|
1287
|
-
return null
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
private endpointKey(address: string): string {
|
|
1292
|
-
const parsed = this.parseAddress(address)
|
|
1293
|
-
if (!parsed) return address
|
|
1294
|
-
const host = this.hostsEquivalent(parsed.host, 'localhost')
|
|
1295
|
-
? 'localhost'
|
|
1296
|
-
: parsed.host
|
|
1297
|
-
return `${host}:${parsed.port}`
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
private hostsEquivalent(a: string, b: string): boolean {
|
|
1301
|
-
if (a === b) return true
|
|
1302
|
-
|
|
1303
|
-
const loopback = new Set(['localhost', '127.0.0.1', '::1'])
|
|
1304
|
-
return loopback.has(a) && loopback.has(b)
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
private closeAllSockets(): void {
|
|
1308
|
-
for (const timer of this.reconnectTimers.values()) clearTimeout(timer)
|
|
1309
|
-
this.reconnectTimers.clear()
|
|
1310
|
-
this.endpointSockets.clear()
|
|
1311
|
-
|
|
1312
|
-
for (const m of this.sockets.values()) {
|
|
1313
|
-
m.ws.onclose = null
|
|
1314
|
-
m.ws.onerror = null
|
|
1315
|
-
m.ws.onopen = null
|
|
1316
|
-
m.ws.onmessage = null
|
|
1317
|
-
m.ws.close()
|
|
1318
|
-
}
|
|
1319
|
-
this.sockets.clear()
|
|
1320
|
-
this.setCurrentServer(null, 'all sockets closed')
|
|
1321
|
-
this.setConnectionStatus(
|
|
1322
|
-
ConnectionStatus.Disconnected,
|
|
1323
|
-
'all sockets closed',
|
|
1324
|
-
)
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
private createSocket(address: string, reconnectOnClose = true): void {
|
|
1328
|
-
const endpointKey = this.endpointKey(address)
|
|
1329
|
-
if (this.endpointSockets.has(endpointKey)) return
|
|
1330
|
-
this.endpointSockets.set(endpointKey, address)
|
|
1331
|
-
const reconnectTimer = this.reconnectTimers.get(endpointKey)
|
|
1332
|
-
if (reconnectTimer) {
|
|
1333
|
-
clearTimeout(reconnectTimer)
|
|
1334
|
-
this.reconnectTimers.delete(endpointKey)
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
this.logConnection('socket_connecting', {
|
|
1338
|
-
address,
|
|
1339
|
-
openServers: this.getOpenServers(),
|
|
1340
|
-
knownServers: this.getServers(),
|
|
1341
|
-
})
|
|
1342
|
-
const ws = new WebSocket(address)
|
|
1343
|
-
ws.binaryType = 'arraybuffer' // Receive binary messages as ArrayBuffer for msgpack
|
|
1344
|
-
const managed: ManagedSocket = {
|
|
1345
|
-
ws,
|
|
1346
|
-
address,
|
|
1347
|
-
endpointKey,
|
|
1348
|
-
reconnectOnClose,
|
|
1349
|
-
}
|
|
1350
|
-
this.sockets.set(address, managed)
|
|
1351
|
-
|
|
1352
|
-
if (this.sockets.size === 1) {
|
|
1353
|
-
this.setConnectionStatus(
|
|
1354
|
-
ConnectionStatus.Connecting,
|
|
1355
|
-
`connecting to ${address}`,
|
|
1356
|
-
)
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
ws.onopen = () => {
|
|
1360
|
-
if (this.sockets.get(address) !== managed) {
|
|
1361
|
-
ws.close()
|
|
1362
|
-
return
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
if (!this.currentServer) {
|
|
1366
|
-
this.setCurrentServer(address, 'socket open')
|
|
1367
|
-
this.setConnectionStatus(
|
|
1368
|
-
ConnectionStatus.Connected,
|
|
1369
|
-
`connected to ${address}`,
|
|
1370
|
-
)
|
|
1371
|
-
this.flushQueue()
|
|
1372
|
-
this.resendSubscriptions()
|
|
1373
|
-
if (this.peerDiscoveryEnabled) this.startPeerDiscovery()
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
ws.onclose = () => {
|
|
1378
|
-
if (this.sockets.get(address) !== managed) return
|
|
1379
|
-
|
|
1380
|
-
this.sockets.delete(address)
|
|
1381
|
-
this.endpointSockets.delete(endpointKey)
|
|
1382
|
-
|
|
1383
|
-
if (this.currentServer === address) {
|
|
1384
|
-
const next = this.getFirstOpenSocket()
|
|
1385
|
-
if (next) {
|
|
1386
|
-
this.setCurrentServer(
|
|
1387
|
-
next.address,
|
|
1388
|
-
`failover from ${address} to ${next.address}`,
|
|
1389
|
-
)
|
|
1390
|
-
this.setConnectionStatus(
|
|
1391
|
-
ConnectionStatus.Connected,
|
|
1392
|
-
`main failover to ${next.address}`,
|
|
1393
|
-
)
|
|
1394
|
-
this.resendSubscriptions()
|
|
1395
|
-
return // Peer discovery will re-add this server when it's back
|
|
1396
|
-
}
|
|
1397
|
-
this.setCurrentServer(null, `disconnected from ${address}`)
|
|
1398
|
-
this.setConnectionStatus(
|
|
1399
|
-
ConnectionStatus.Disconnected,
|
|
1400
|
-
`no open servers after ${address} closed`,
|
|
1401
|
-
)
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
// Only retry explicitly configured sockets when completely disconnected.
|
|
1405
|
-
// Discovered peers are expected to reappear via peer discovery.
|
|
1406
|
-
if (
|
|
1407
|
-
managed.reconnectOnClose &&
|
|
1408
|
-
this.shouldReconnect &&
|
|
1409
|
-
!this.hasOpenConnection()
|
|
1410
|
-
) {
|
|
1411
|
-
this.scheduleReconnect(address, endpointKey)
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
ws.onerror = () => {}
|
|
1416
|
-
|
|
1417
|
-
ws.onmessage = (event) => {
|
|
1418
|
-
if (this.sockets.get(address) !== managed) return
|
|
1419
|
-
this.onMessage(event.data)
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
private scheduleReconnect(address: string, endpointKey: string): void {
|
|
1424
|
-
if (this.reconnectTimers.has(endpointKey)) return
|
|
1425
|
-
|
|
1426
|
-
this.logConnection('reconnect_scheduled', { address, delayMs: 1000 })
|
|
1427
|
-
const timer = setTimeout(() => {
|
|
1428
|
-
this.reconnectTimers.delete(endpointKey)
|
|
1429
|
-
if (
|
|
1430
|
-
this.shouldReconnect &&
|
|
1431
|
-
!this.hasOpenConnection() &&
|
|
1432
|
-
!this.endpointSockets.has(endpointKey)
|
|
1433
|
-
) {
|
|
1434
|
-
this.logConnection('reconnect_attempt', { address })
|
|
1435
|
-
this.createSocket(address)
|
|
1436
|
-
}
|
|
1437
|
-
}, 1000)
|
|
1438
|
-
|
|
1439
|
-
this.reconnectTimers.set(endpointKey, timer)
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1443
|
-
// Private: Message Handling
|
|
1444
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1445
|
-
|
|
1446
|
-
private onMessage(data: string | ArrayBuffer | Blob): void {
|
|
1447
|
-
this.downMsgCounter.next()
|
|
1448
|
-
|
|
1449
|
-
try {
|
|
1450
|
-
let message: MykoMessage
|
|
1451
|
-
|
|
1452
|
-
if (typeof data === 'string') {
|
|
1453
|
-
// JSON text message
|
|
1454
|
-
message = JSON.parse(data) as MykoMessage
|
|
1455
|
-
} else if (data instanceof ArrayBuffer) {
|
|
1456
|
-
// Binary msgpack message
|
|
1457
|
-
message = unpackr.unpack(new Uint8Array(data)) as MykoMessage
|
|
1458
|
-
} else if (data instanceof Blob) {
|
|
1459
|
-
// Handle Blob asynchronously - convert to ArrayBuffer first
|
|
1460
|
-
data.arrayBuffer().then((buffer) => {
|
|
1461
|
-
const decoded = unpackr.unpack(new Uint8Array(buffer)) as MykoMessage
|
|
1462
|
-
this.routeMessage(decoded)
|
|
1463
|
-
})
|
|
1464
|
-
return
|
|
1465
|
-
} else {
|
|
1466
|
-
return
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
this.routeMessage(message)
|
|
1470
|
-
} catch {
|
|
1471
|
-
// Ignore parse errors
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
private routeMessage(message: MykoMessage): void {
|
|
1476
|
-
switch (message.event) {
|
|
1477
|
-
case MykoEvent.QueryResponse:
|
|
1478
|
-
this.maybeLogFirstResponseTiming('query', message.data.tx, {
|
|
1479
|
-
queryId: this.activeQueries.get(message.data.tx)?.queryId,
|
|
1480
|
-
queryName:
|
|
1481
|
-
this.activeQueryNames.get(message.data.tx) ??
|
|
1482
|
-
this.activeQueries.get(message.data.tx)?.queryId ??
|
|
1483
|
-
'unknown',
|
|
1484
|
-
sequence: message.data.sequence,
|
|
1485
|
-
upserts: message.data.upserts.length,
|
|
1486
|
-
deletes: message.data.deletes.length,
|
|
1487
|
-
})
|
|
1488
|
-
this.logConnection('query_response', {
|
|
1489
|
-
tx: message.data.tx,
|
|
1490
|
-
queryId: this.activeQueries.get(message.data.tx)?.queryId,
|
|
1491
|
-
queryItemType: this.activeQueries.get(message.data.tx)?.queryItemType,
|
|
1492
|
-
queryName:
|
|
1493
|
-
this.activeQueryNames.get(message.data.tx) ??
|
|
1494
|
-
this.activeQueries.get(message.data.tx)?.queryId ??
|
|
1495
|
-
'unknown',
|
|
1496
|
-
sequence: message.data.sequence,
|
|
1497
|
-
upserts: message.data.upserts.length,
|
|
1498
|
-
deletes: message.data.deletes.length,
|
|
1499
|
-
changes: (message.data as { changes?: unknown[] }).changes?.length ?? 0,
|
|
1500
|
-
totalCount:
|
|
1501
|
-
(message.data as { totalCount?: number; total_count?: number }).totalCount ??
|
|
1502
|
-
(message.data as { totalCount?: number; total_count?: number }).total_count ??
|
|
1503
|
-
null,
|
|
1504
|
-
window: (message.data as { window?: QueryWindow | null }).window ?? null,
|
|
1505
|
-
})
|
|
1506
|
-
const queryPublishStarted = this.nowMs()
|
|
1507
|
-
this.queryResponses.next(message)
|
|
1508
|
-
this.logConnection('query_publish_ms', {
|
|
1509
|
-
tx: message.data.tx,
|
|
1510
|
-
sequence: message.data.sequence,
|
|
1511
|
-
publishMs: Number((this.nowMs() - queryPublishStarted).toFixed(2)),
|
|
1512
|
-
})
|
|
1513
|
-
break
|
|
1514
|
-
case MykoEvent.ViewResponse:
|
|
1515
|
-
// ViewResponse and QueryResponse share the same payload shape.
|
|
1516
|
-
const viewMessage = message as unknown as QueryResponseMessage
|
|
1517
|
-
this.maybeLogFirstResponseTiming(
|
|
1518
|
-
'view',
|
|
1519
|
-
viewMessage.data.tx,
|
|
1520
|
-
{
|
|
1521
|
-
viewId: this.activeViews.get(viewMessage.data.tx)?.viewId,
|
|
1522
|
-
viewName:
|
|
1523
|
-
this.activeViewNames.get(viewMessage.data.tx) ??
|
|
1524
|
-
this.activeViews.get(viewMessage.data.tx)?.viewId ??
|
|
1525
|
-
'unknown',
|
|
1526
|
-
sequence: viewMessage.data.sequence,
|
|
1527
|
-
upserts: viewMessage.data.upserts.length,
|
|
1528
|
-
deletes: viewMessage.data.deletes.length,
|
|
1529
|
-
},
|
|
1530
|
-
)
|
|
1531
|
-
this.logConnection('view_response', {
|
|
1532
|
-
tx: viewMessage.data.tx,
|
|
1533
|
-
sequence: viewMessage.data.sequence,
|
|
1534
|
-
upserts: viewMessage.data.upserts.length,
|
|
1535
|
-
deletes: viewMessage.data.deletes.length,
|
|
1536
|
-
changes: (viewMessage.data as { changes?: unknown[] }).changes?.length ?? 0,
|
|
1537
|
-
totalCount: (viewMessage.data as { totalCount?: number; total_count?: number })
|
|
1538
|
-
.totalCount ??
|
|
1539
|
-
(viewMessage.data as { totalCount?: number; total_count?: number }).total_count ??
|
|
1540
|
-
null,
|
|
1541
|
-
window: (viewMessage.data as { window?: QueryWindow | null }).window ?? null,
|
|
1542
|
-
})
|
|
1543
|
-
const viewPublishStarted = this.nowMs()
|
|
1544
|
-
this.queryResponses.next(viewMessage)
|
|
1545
|
-
this.logConnection('view_publish_ms', {
|
|
1546
|
-
tx: viewMessage.data.tx,
|
|
1547
|
-
sequence: viewMessage.data.sequence,
|
|
1548
|
-
publishMs: Number((this.nowMs() - viewPublishStarted).toFixed(2)),
|
|
1549
|
-
})
|
|
1550
|
-
break
|
|
1551
|
-
case MykoEvent.ReportResponse:
|
|
1552
|
-
this.reportResponses.next(message)
|
|
1553
|
-
break
|
|
1554
|
-
case MykoEvent.CommandResponse:
|
|
1555
|
-
this.commandResponses.next(message as CommandResponseMessage)
|
|
1556
|
-
break
|
|
1557
|
-
case MykoEvent.CommandError:
|
|
1558
|
-
this.commandErrors.next(message as CommandErrorMessage)
|
|
1559
|
-
break
|
|
1560
|
-
case MykoEvent.QueryError:
|
|
1561
|
-
this.queryErrors.next(message as QueryErrorMessage)
|
|
1562
|
-
this.logConnection('query_error', {
|
|
1563
|
-
tx: message.data.tx,
|
|
1564
|
-
message: message.data.message,
|
|
1565
|
-
queryId: this.activeQueries.get(message.data.tx)?.queryId,
|
|
1566
|
-
queryItemType: this.activeQueries.get(message.data.tx)?.queryItemType,
|
|
1567
|
-
queryName:
|
|
1568
|
-
this.activeQueryNames.get(message.data.tx) ??
|
|
1569
|
-
this.activeQueries.get(message.data.tx)?.queryId ??
|
|
1570
|
-
'unknown',
|
|
1571
|
-
})
|
|
1572
|
-
break
|
|
1573
|
-
case MykoEvent.ViewError:
|
|
1574
|
-
this.queryErrors.next(message as unknown as QueryErrorMessage)
|
|
1575
|
-
this.logConnection('view_error', {
|
|
1576
|
-
tx: message.data.tx,
|
|
1577
|
-
message: message.data.message,
|
|
1578
|
-
viewId: this.activeViews.get(message.data.tx)?.viewId,
|
|
1579
|
-
viewItemType: this.activeViews.get(message.data.tx)?.viewItemType,
|
|
1580
|
-
viewName:
|
|
1581
|
-
this.activeViewNames.get(message.data.tx) ??
|
|
1582
|
-
this.activeViews.get(message.data.tx)?.viewId ??
|
|
1583
|
-
'unknown',
|
|
1584
|
-
})
|
|
1585
|
-
break
|
|
1586
|
-
case MykoEvent.ReportError:
|
|
1587
|
-
this.reportErrors.next(message as ReportErrorMessage)
|
|
1588
|
-
this.logConnection('report_error', {
|
|
1589
|
-
tx: message.data.tx,
|
|
1590
|
-
message: message.data.message,
|
|
1591
|
-
reportId: this.activeReports.get(message.data.tx)?.reportId,
|
|
1592
|
-
reportName:
|
|
1593
|
-
this.activeReportNames.get(message.data.tx) ??
|
|
1594
|
-
this.activeReports.get(message.data.tx)?.reportId ??
|
|
1595
|
-
'unknown',
|
|
1596
|
-
})
|
|
1597
|
-
break
|
|
1598
|
-
case MykoEvent.Ping:
|
|
1599
|
-
this.pingResponses.next(message as PingMessage)
|
|
1600
|
-
break
|
|
1601
|
-
case MykoEvent.Command:
|
|
1602
|
-
this.commandIncoming.next(message as CommandIncomingMessage)
|
|
1603
|
-
break
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
private scheduleEventBatchFlush(): void {
|
|
1608
|
-
if (this.eventBatchFlushScheduled) return
|
|
1609
|
-
this.eventBatchFlushScheduled = true
|
|
1610
|
-
queueMicrotask(() => {
|
|
1611
|
-
this.eventBatchFlushScheduled = false
|
|
1612
|
-
this.flushPendingEventBatch()
|
|
1613
|
-
})
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
private flushPendingEventBatch(): void {
|
|
1617
|
-
if (this.pendingEventBatch.length === 0) return
|
|
1618
|
-
|
|
1619
|
-
while (this.pendingEventBatch.length > 0) {
|
|
1620
|
-
const batch = this.pendingEventBatch.splice(0, this.eventBatchMaxSize)
|
|
1621
|
-
this.sendNow({ event: EVENT_BATCH, data: batch } as unknown as MykoMessage)
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
private send(message: MykoMessage): void {
|
|
1626
|
-
if ((message as { event: string }).event !== EVENT_BATCH) {
|
|
1627
|
-
this.flushPendingEventBatch()
|
|
1628
|
-
}
|
|
1629
|
-
this.sendNow(message)
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
private messageTx(message: MykoMessage): string | null {
|
|
1633
|
-
const data = (message as { data?: unknown }).data as
|
|
1634
|
-
| { tx?: string }
|
|
1635
|
-
| undefined
|
|
1636
|
-
return typeof data?.tx === 'string' ? data.tx : null
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
private sendNow(message: MykoMessage): void {
|
|
1640
|
-
const event = (message as { event: string }).event
|
|
1641
|
-
const tx = this.messageTx(message)
|
|
1642
|
-
if (this.currentServer) {
|
|
1643
|
-
const managed = this.sockets.get(this.currentServer)
|
|
1644
|
-
if (managed?.ws.readyState === WebSocket.OPEN) {
|
|
1645
|
-
const encoded =
|
|
1646
|
-
this.protocol === MykoProtocol.MSGPACK
|
|
1647
|
-
? new Uint8Array(packr.pack(message))
|
|
1648
|
-
: JSON.stringify(message)
|
|
1649
|
-
managed.ws.send(encoded)
|
|
1650
|
-
this.upMsgCounter.next()
|
|
1651
|
-
return
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
this.messageQueue.push(message)
|
|
1655
|
-
this.logConnection('ws_enqueue', {
|
|
1656
|
-
event,
|
|
1657
|
-
tx,
|
|
1658
|
-
queueDepth: this.messageQueue.length,
|
|
1659
|
-
currentServer: this.currentServer,
|
|
1660
|
-
})
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
private flushQueue(): void {
|
|
1664
|
-
const queue = this.messageQueue
|
|
1665
|
-
this.messageQueue = []
|
|
1666
|
-
this.logConnection('ws_flush_queue', {
|
|
1667
|
-
queuedMessages: queue.length,
|
|
1668
|
-
currentServer: this.currentServer,
|
|
1669
|
-
})
|
|
1670
|
-
for (const msg of queue) this.send(msg)
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
private withReconnectSequenceReset(query: WrappedQuery): WrappedQuery {
|
|
1674
|
-
const queryPayload = query.query
|
|
1675
|
-
if (
|
|
1676
|
-
queryPayload &&
|
|
1677
|
-
typeof queryPayload === 'object' &&
|
|
1678
|
-
!Array.isArray(queryPayload)
|
|
1679
|
-
) {
|
|
1680
|
-
return {
|
|
1681
|
-
...query,
|
|
1682
|
-
query: {
|
|
1683
|
-
...(queryPayload as Record<string, JsonValue>),
|
|
1684
|
-
seq: 0 as JsonValue,
|
|
1685
|
-
},
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
return query
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
private resendSubscriptions(): void {
|
|
1692
|
-
for (const q of this.activeQueries.values()) {
|
|
1693
|
-
this.send({
|
|
1694
|
-
event: MykoEvent.Query,
|
|
1695
|
-
data: this.withReconnectSequenceReset(q),
|
|
1696
|
-
})
|
|
1697
|
-
}
|
|
1698
|
-
for (const v of this.activeViews.values()) {
|
|
1699
|
-
this.send({ event: MykoEvent.View, data: v })
|
|
1700
|
-
}
|
|
1701
|
-
for (const r of this.activeReports.values()) {
|
|
1702
|
-
this.send({ event: MykoEvent.Report, data: r })
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
private setConnectionStatus(status: ConnectionStatus, reason: string): void {
|
|
1707
|
-
this.connectionStatusSubject.next(status)
|
|
1708
|
-
this.logConnection('status', {
|
|
1709
|
-
status,
|
|
1710
|
-
reason,
|
|
1711
|
-
currentServer: this.currentServer,
|
|
1712
|
-
openServers: this.getOpenServers(),
|
|
1713
|
-
})
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
private setCurrentServer(server: string | null, reason: string): void {
|
|
1717
|
-
this.currentServer = server
|
|
1718
|
-
this.currentServerSubject.next(server)
|
|
1719
|
-
this.logConnection('current_server', { server, reason })
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
private nowMs(): number {
|
|
1723
|
-
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
|
|
1724
|
-
return performance.now()
|
|
1725
|
-
}
|
|
1726
|
-
return Date.now()
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
private maybeLogFirstResponseTiming(
|
|
1730
|
-
kind: 'query' | 'view',
|
|
1731
|
-
tx: string,
|
|
1732
|
-
details: Record<string, unknown>,
|
|
1733
|
-
): void {
|
|
1734
|
-
if (this.firstResponseLogged.has(tx)) return
|
|
1735
|
-
const startedAt = this.subscriptionStartMs.get(tx)
|
|
1736
|
-
if (startedAt === undefined) return
|
|
1737
|
-
const elapsedMs = this.nowMs() - startedAt
|
|
1738
|
-
this.firstResponseLogged.add(tx)
|
|
1739
|
-
this.logConnection(`${kind}_first_response_timing`, {
|
|
1740
|
-
tx,
|
|
1741
|
-
subscribeToFirstResponseMs: Number(elapsedMs.toFixed(2)),
|
|
1742
|
-
...details,
|
|
1743
|
-
})
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
private connectionLogLevel(
|
|
1747
|
-
event: string,
|
|
1748
|
-
): 'info' | 'debug' | 'verbose' | 'warn' | 'error' {
|
|
1749
|
-
// Main lifecycle state transitions remain visible at info.
|
|
1750
|
-
const infoEvents = new Set(['status', 'current_server'])
|
|
1751
|
-
if (infoEvents.has(event)) return 'info'
|
|
1752
|
-
|
|
1753
|
-
// Subscription setup and first-response timing are debug-level diagnostics.
|
|
1754
|
-
const debugEvents = new Set([
|
|
1755
|
-
'socket_connecting',
|
|
1756
|
-
'peer_discovery_started',
|
|
1757
|
-
'peer_discovery_update',
|
|
1758
|
-
'query_subscribe',
|
|
1759
|
-
'view_subscribe',
|
|
1760
|
-
'report_subscribe',
|
|
1761
|
-
'query_cancel',
|
|
1762
|
-
'view_cancel',
|
|
1763
|
-
'report_cancel',
|
|
1764
|
-
'query_first_response_timing',
|
|
1765
|
-
'view_first_response_timing',
|
|
1766
|
-
'reconnect_scheduled',
|
|
1767
|
-
'reconnect_attempt',
|
|
1768
|
-
'query_shape_invalid',
|
|
1769
|
-
])
|
|
1770
|
-
if (debugEvents.has(event)) return 'debug'
|
|
1771
|
-
|
|
1772
|
-
// Follow-up response churn and transport internals are verbose-level.
|
|
1773
|
-
const verboseEvents = new Set([
|
|
1774
|
-
'ws_enqueue',
|
|
1775
|
-
'ws_flush_queue',
|
|
1776
|
-
'query_response',
|
|
1777
|
-
'view_response',
|
|
1778
|
-
'report_response',
|
|
1779
|
-
'query_publish_ms',
|
|
1780
|
-
'view_publish_ms',
|
|
1781
|
-
])
|
|
1782
|
-
if (verboseEvents.has(event)) return 'verbose'
|
|
1783
|
-
|
|
1784
|
-
if (event.endsWith('_error')) return 'error'
|
|
1785
|
-
|
|
1786
|
-
return 'debug'
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
private static resolveDefaultConnectionLogLevel(): ConnectionLogLevel {
|
|
1790
|
-
const readGlobal = (): string | undefined => {
|
|
1791
|
-
const globalObj = globalThis as Record<string, unknown>
|
|
1792
|
-
const value = globalObj.MYKO_CLIENT_LOG_LEVEL
|
|
1793
|
-
return typeof value === 'string' ? value : undefined
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
const readProcessEnv = (): string | undefined => {
|
|
1797
|
-
const processLike = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process
|
|
1798
|
-
return processLike?.env?.MYKO_CLIENT_LOG_LEVEL
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
const value = (readGlobal() ?? readProcessEnv() ?? '').toLowerCase()
|
|
1802
|
-
switch (value) {
|
|
1803
|
-
case 'silent':
|
|
1804
|
-
case 'error':
|
|
1805
|
-
case 'warn':
|
|
1806
|
-
case 'info':
|
|
1807
|
-
case 'debug':
|
|
1808
|
-
case 'verbose':
|
|
1809
|
-
return value
|
|
1810
|
-
default:
|
|
1811
|
-
return 'warn'
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
private shouldLogConnection(level: ConnectionLogLevel): boolean {
|
|
1816
|
-
const priority: Record<ConnectionLogLevel, number> = {
|
|
1817
|
-
silent: 0,
|
|
1818
|
-
error: 1,
|
|
1819
|
-
warn: 2,
|
|
1820
|
-
info: 3,
|
|
1821
|
-
debug: 4,
|
|
1822
|
-
verbose: 5,
|
|
1823
|
-
}
|
|
1824
|
-
return priority[level] <= priority[this.connectionLogLevelThreshold]
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
private logConnection(event: string, details: Record<string, unknown>): void {
|
|
1828
|
-
const level = this.connectionLogLevel(event)
|
|
1829
|
-
if (!this.shouldLogConnection(level)) return
|
|
1830
|
-
const maybeVerbose = (
|
|
1831
|
-
console as unknown as { verbose?: (...args: unknown[]) => void }
|
|
1832
|
-
).verbose
|
|
1833
|
-
const logger: (...args: unknown[]) => void =
|
|
1834
|
-
level === 'info'
|
|
1835
|
-
? console.info
|
|
1836
|
-
: level === 'warn'
|
|
1837
|
-
? console.warn
|
|
1838
|
-
: level === 'error'
|
|
1839
|
-
? console.error
|
|
1840
|
-
: level === 'verbose' || level === 'debug'
|
|
1841
|
-
? maybeVerbose ?? console.debug ?? console.log
|
|
1842
|
-
: console.log
|
|
1843
|
-
const levelPrefix = level === 'verbose' ? '[verbose] ' : ''
|
|
1844
|
-
|
|
1845
|
-
logger(`${levelPrefix}[MykoClient] ${event}`, {
|
|
1846
|
-
tsIso: new Date().toISOString(),
|
|
1847
|
-
tsMs: Date.now(),
|
|
1848
|
-
...details,
|
|
1849
|
-
})
|
|
1850
|
-
}
|
|
1851
|
-
}
|