@dxos/client-services 0.6.13-main.548ca8d → 0.6.13-staging.1e988a3

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.
Files changed (118) hide show
  1. package/dist/lib/browser/{chunk-UEQIHAL2.mjs → chunk-4XKSDWEO.mjs} +5218 -5905
  2. package/dist/lib/browser/chunk-4XKSDWEO.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +3 -3
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +7 -8
  7. package/dist/lib/browser/testing/index.mjs.map +3 -3
  8. package/dist/lib/node/{chunk-MA5EWTRH.cjs → chunk-HNOOWB6F.cjs} +5060 -5743
  9. package/dist/lib/node/chunk-HNOOWB6F.cjs.map +7 -0
  10. package/dist/lib/node/index.cjs +46 -46
  11. package/dist/lib/node/index.cjs.map +3 -3
  12. package/dist/lib/node/meta.json +1 -1
  13. package/dist/lib/node/testing/index.cjs +13 -14
  14. package/dist/lib/node/testing/index.cjs.map +3 -3
  15. package/dist/types/src/packlets/diagnostics/diagnostics-broadcast.d.ts.map +1 -1
  16. package/dist/types/src/packlets/identity/authenticator.d.ts.map +1 -1
  17. package/dist/types/src/packlets/identity/authenticator.test.d.ts +2 -0
  18. package/dist/types/src/packlets/identity/authenticator.test.d.ts.map +1 -0
  19. package/dist/types/src/packlets/identity/contacts-service.d.ts +1 -1
  20. package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -1
  21. package/dist/types/src/packlets/identity/identity-manager.d.ts +7 -19
  22. package/dist/types/src/packlets/identity/identity-manager.d.ts.map +1 -1
  23. package/dist/types/src/packlets/identity/identity.d.ts +1 -8
  24. package/dist/types/src/packlets/identity/identity.d.ts.map +1 -1
  25. package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts +1 -2
  26. package/dist/types/src/packlets/invitations/invitation-guest-extenstion.d.ts.map +1 -1
  27. package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts +1 -2
  28. package/dist/types/src/packlets/invitations/invitation-host-extension.d.ts.map +1 -1
  29. package/dist/types/src/packlets/invitations/invitations-handler.d.ts +8 -8
  30. package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
  31. package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
  32. package/dist/types/src/packlets/services/automerge-host.test.d.ts +2 -0
  33. package/dist/types/src/packlets/services/automerge-host.test.d.ts.map +1 -0
  34. package/dist/types/src/packlets/services/service-context.d.ts +9 -9
  35. package/dist/types/src/packlets/services/service-context.d.ts.map +1 -1
  36. package/dist/types/src/packlets/services/service-host.d.ts +0 -1
  37. package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
  38. package/dist/types/src/packlets/spaces/data-space-manager.d.ts +3 -6
  39. package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
  40. package/dist/types/src/packlets/spaces/data-space.d.ts +3 -4
  41. package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
  42. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts +0 -3
  43. package/dist/types/src/packlets/spaces/edge-feed-replicator.d.ts.map +1 -1
  44. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts +1 -1
  45. package/dist/types/src/packlets/spaces/epoch-migrations.d.ts.map +1 -1
  46. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts +6 -31
  47. package/dist/types/src/packlets/spaces/notarization-plugin.d.ts.map +1 -1
  48. package/dist/types/src/packlets/spaces/spaces-service.d.ts +1 -1
  49. package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
  50. package/dist/types/src/packlets/storage/storage.d.ts.map +1 -1
  51. package/dist/types/src/packlets/testing/test-builder.d.ts +2 -1
  52. package/dist/types/src/packlets/testing/test-builder.d.ts.map +1 -1
  53. package/dist/types/src/packlets/worker/worker-runtime.d.ts.map +1 -1
  54. package/dist/types/src/version.d.ts +1 -1
  55. package/dist/types/src/version.d.ts.map +1 -1
  56. package/package.json +39 -43
  57. package/src/packlets/devices/devices-service.test.ts +5 -4
  58. package/src/packlets/diagnostics/diagnostics-broadcast.ts +0 -1
  59. package/src/packlets/identity/{authenticator.node.test.ts → authenticator.test.ts} +3 -2
  60. package/src/packlets/identity/authenticator.ts +2 -5
  61. package/src/packlets/identity/contacts-service.ts +1 -1
  62. package/src/packlets/identity/identity-manager.test.ts +6 -5
  63. package/src/packlets/identity/identity-manager.ts +19 -35
  64. package/src/packlets/identity/identity-service.test.ts +8 -4
  65. package/src/packlets/identity/identity.test.ts +239 -128
  66. package/src/packlets/identity/identity.ts +8 -42
  67. package/src/packlets/invitations/device-invitation-protocol.test.ts +4 -7
  68. package/src/packlets/invitations/invitation-guest-extenstion.ts +4 -8
  69. package/src/packlets/invitations/invitation-host-extension.ts +7 -8
  70. package/src/packlets/invitations/invitations-handler.test.ts +9 -16
  71. package/src/packlets/invitations/invitations-handler.ts +92 -23
  72. package/src/packlets/invitations/space-invitation-protocol.test.ts +3 -4
  73. package/src/packlets/invitations/space-invitation-protocol.ts +0 -4
  74. package/src/packlets/logging/logging.test.ts +2 -1
  75. package/src/packlets/network/network-service.test.ts +3 -2
  76. package/src/packlets/services/automerge-host.test.ts +60 -0
  77. package/src/packlets/services/service-context.test.ts +1 -3
  78. package/src/packlets/services/service-context.ts +31 -68
  79. package/src/packlets/services/service-host.test.ts +12 -8
  80. package/src/packlets/services/service-host.ts +6 -8
  81. package/src/packlets/services/service-registry.test.ts +2 -1
  82. package/src/packlets/spaces/data-space-manager.test.ts +2 -2
  83. package/src/packlets/spaces/data-space-manager.ts +5 -40
  84. package/src/packlets/spaces/data-space.ts +6 -34
  85. package/src/packlets/spaces/edge-feed-replicator.ts +22 -80
  86. package/src/packlets/spaces/epoch-migrations.ts +2 -2
  87. package/src/packlets/spaces/notarization-plugin.test.ts +7 -10
  88. package/src/packlets/spaces/notarization-plugin.ts +29 -169
  89. package/src/packlets/spaces/spaces-service.test.ts +9 -5
  90. package/src/packlets/spaces/spaces-service.ts +1 -6
  91. package/src/packlets/storage/storage.ts +1 -0
  92. package/src/packlets/system/system-service.test.ts +2 -1
  93. package/src/packlets/testing/test-builder.ts +4 -3
  94. package/src/packlets/worker/worker-runtime.ts +2 -2
  95. package/src/version.ts +1 -1
  96. package/dist/lib/browser/chunk-UEQIHAL2.mjs.map +0 -7
  97. package/dist/lib/node/chunk-MA5EWTRH.cjs.map +0 -7
  98. package/dist/lib/node-esm/chunk-AIBLDI4U.mjs +0 -8403
  99. package/dist/lib/node-esm/chunk-AIBLDI4U.mjs.map +0 -7
  100. package/dist/lib/node-esm/index.mjs +0 -416
  101. package/dist/lib/node-esm/index.mjs.map +0 -7
  102. package/dist/lib/node-esm/meta.json +0 -1
  103. package/dist/lib/node-esm/testing/index.mjs +0 -420
  104. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  105. package/dist/types/src/packlets/identity/authenticator.node.test.d.ts +0 -2
  106. package/dist/types/src/packlets/identity/authenticator.node.test.d.ts.map +0 -1
  107. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts +0 -30
  108. package/dist/types/src/packlets/invitations/edge-invitation-handler.d.ts.map +0 -1
  109. package/dist/types/src/packlets/invitations/invitation-state.d.ts +0 -19
  110. package/dist/types/src/packlets/invitations/invitation-state.d.ts.map +0 -1
  111. package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts +0 -2
  112. package/dist/types/src/packlets/spaces/edge-feed-replicator.test.d.ts.map +0 -1
  113. package/dist/types/src/testing/setup.d.ts +0 -3
  114. package/dist/types/src/testing/setup.d.ts.map +0 -1
  115. package/src/packlets/invitations/edge-invitation-handler.ts +0 -184
  116. package/src/packlets/invitations/invitation-state.ts +0 -111
  117. package/src/packlets/spaces/edge-feed-replicator.test.ts +0 -253
  118. package/src/testing/setup.ts +0 -11
@@ -1,184 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- import { type MutexGuard, scheduleMicroTask, scheduleTask } from '@dxos/async';
6
- import { type Context } from '@dxos/context';
7
- import { ed25519Signature } from '@dxos/crypto';
8
- import { type EdgeHttpClient } from '@dxos/edge-client';
9
- import { invariant } from '@dxos/invariant';
10
- import { SpaceId } from '@dxos/keys';
11
- import { log } from '@dxos/log';
12
- import {
13
- EdgeAuthChallengeError,
14
- EdgeCallFailedError,
15
- type JoinSpaceRequest,
16
- type JoinSpaceResponseBody,
17
- } from '@dxos/protocols';
18
- import { schema } from '@dxos/protocols/proto';
19
- import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
20
- import { type DeviceProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
21
- import {
22
- type AdmissionResponse,
23
- type AdmissionRequest,
24
- type SpaceAdmissionRequest,
25
- } from '@dxos/protocols/proto/dxos/halo/invitations';
26
-
27
- import { type InvitationProtocol } from './invitation-protocol';
28
- import { type FlowLockHolder, type GuardedInvitationState } from './invitation-state';
29
- import { tryAcquireBeforeContextDisposed } from './utils';
30
-
31
- export interface EdgeInvitationHandlerCallbacks {
32
- onInvitationSuccess(response: AdmissionResponse, request: AdmissionRequest): Promise<void>;
33
- }
34
-
35
- export const DEFAULT_REQUEST_RETRY_INTERVAL_MS = 3000;
36
- export const DEFAULT_REQUEST_RETRY_JITTER_MS = 500;
37
-
38
- export type EdgeInvitationConfig = {
39
- retryInterval?: number;
40
- retryJitter?: number;
41
- };
42
-
43
- export class EdgeInvitationHandler implements FlowLockHolder {
44
- private _flowLock: MutexGuard | undefined;
45
-
46
- private readonly _retryInterval: number;
47
- private readonly _retryJitter: number;
48
-
49
- constructor(
50
- config: EdgeInvitationConfig | undefined,
51
- private readonly _client: EdgeHttpClient | undefined,
52
- private readonly _callbacks: EdgeInvitationHandlerCallbacks,
53
- ) {
54
- this._retryInterval = config?.retryInterval ?? DEFAULT_REQUEST_RETRY_INTERVAL_MS;
55
- this._retryJitter = config?.retryJitter ?? DEFAULT_REQUEST_RETRY_JITTER_MS;
56
- }
57
-
58
- public handle(
59
- ctx: Context,
60
- guardedState: GuardedInvitationState,
61
- protocol: InvitationProtocol,
62
- deviceProfile?: DeviceProfileDocument,
63
- ) {
64
- if (!this._client) {
65
- log('edge disabled');
66
- return;
67
- }
68
-
69
- const invitation = guardedState.current;
70
- const spaceId = invitation.spaceId;
71
- const canBeHandledByEdge =
72
- invitation.authMethod !== Invitation.AuthMethod.SHARED_SECRET &&
73
- invitation.type === Invitation.Type.DELEGATED &&
74
- invitation.kind === Invitation.Kind.SPACE &&
75
- spaceId != null &&
76
- SpaceId.isValid(spaceId);
77
-
78
- if (!canBeHandledByEdge) {
79
- log('invitation could not be handled by edge', { invitation });
80
- return;
81
- }
82
-
83
- ctx.onDispose(() => {
84
- this._flowLock?.release();
85
- this._flowLock = undefined;
86
- });
87
-
88
- const tryHandleInvitation = async () => {
89
- const admissionRequest = await protocol.createAdmissionRequest(deviceProfile);
90
- if (admissionRequest.space) {
91
- try {
92
- await this._handleSpaceInvitationFlow(ctx, guardedState, admissionRequest.space, spaceId);
93
- } catch (error) {
94
- if (error instanceof EdgeCallFailedError) {
95
- log.info('join space with edge unsuccessful', {
96
- reason: error.message,
97
- retryable: error.isRetryable,
98
- after: error.retryAfterMs ?? this._calculateNextRetryMs(),
99
- });
100
- if (error.isRetryable) {
101
- scheduleTask(ctx, tryHandleInvitation, error.retryAfterMs ?? this._calculateNextRetryMs());
102
- }
103
- } else {
104
- log.info('failed to handle invitation with edge', { error });
105
- scheduleTask(ctx, tryHandleInvitation, this._calculateNextRetryMs());
106
- }
107
- }
108
- }
109
- };
110
- scheduleMicroTask(ctx, tryHandleInvitation);
111
- }
112
-
113
- private async _handleSpaceInvitationFlow(
114
- ctx: Context,
115
- guardedState: GuardedInvitationState,
116
- admissionRequest: SpaceAdmissionRequest,
117
- spaceId: SpaceId,
118
- ) {
119
- try {
120
- log('edge invitation flow');
121
- this._flowLock = await tryAcquireBeforeContextDisposed(ctx, guardedState.mutex);
122
- log('edge invitation flow acquired the lock');
123
-
124
- guardedState.set(this, Invitation.State.CONNECTING);
125
-
126
- const response = await this._joinSpaceByInvitation(guardedState, spaceId, {
127
- identityKey: admissionRequest.identityKey.toHex(),
128
- invitationId: guardedState.current.invitationId,
129
- });
130
-
131
- const admissionResponse = await this._mapToAdmissionResponse(response);
132
- await this._callbacks.onInvitationSuccess(admissionResponse, { space: admissionRequest });
133
- } catch (error) {
134
- guardedState.set(this, Invitation.State.ERROR);
135
- throw error;
136
- } finally {
137
- this._flowLock?.release();
138
- this._flowLock = undefined;
139
- }
140
- }
141
-
142
- private async _mapToAdmissionResponse(edgeResponse: JoinSpaceResponseBody): Promise<AdmissionResponse> {
143
- const credentialBytes = Buffer.from(edgeResponse.spaceMemberCredential, 'base64');
144
- const codec = schema.getCodecForType('dxos.halo.credentials.Credential');
145
- return {
146
- space: {
147
- credential: codec.decode(credentialBytes),
148
- },
149
- };
150
- }
151
-
152
- private async _joinSpaceByInvitation(
153
- guardedState: GuardedInvitationState,
154
- spaceId: SpaceId,
155
- request: JoinSpaceRequest,
156
- ): Promise<JoinSpaceResponseBody> {
157
- invariant(this._client);
158
- try {
159
- return await this._client.joinSpaceByInvitation(spaceId, request);
160
- } catch (error: any) {
161
- if (error instanceof EdgeAuthChallengeError) {
162
- guardedState.set(this, Invitation.State.AUTHENTICATING);
163
- const publicKey = guardedState.current.guestKeypair?.publicKey;
164
- const privateKey = guardedState.current.guestKeypair?.privateKey;
165
- if (!privateKey || !publicKey) {
166
- throw error;
167
- }
168
- const signature = await ed25519Signature(privateKey, Buffer.from(error.challenge, 'base64'));
169
- request.signature = Buffer.from(signature).toString('base64');
170
- return this._client.joinSpaceByInvitation(spaceId, request);
171
- } else {
172
- throw error;
173
- }
174
- }
175
- }
176
-
177
- public hasFlowLock(): boolean {
178
- return this._flowLock != null;
179
- }
180
-
181
- private _calculateNextRetryMs() {
182
- return this._retryInterval + Math.random() * this._retryJitter;
183
- }
184
- }
@@ -1,111 +0,0 @@
1
- /**
2
- * A utility object for serializing invitation state changes by multiple concurrent
3
- * invitation flow connections.
4
- */
5
- //
6
- // Copyright 2024 DXOS.org
7
- //
8
-
9
- import { Mutex, type PushStream } from '@dxos/async';
10
- import { type Context } from '@dxos/context';
11
- import { log } from '@dxos/log';
12
- import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
13
-
14
- import { stateToString } from './utils';
15
-
16
- export interface FlowLockHolder {
17
- hasFlowLock(): boolean;
18
- }
19
-
20
- export interface GuardedInvitationState {
21
- mutex: Mutex;
22
- current: Invitation;
23
-
24
- complete(newState: Partial<Invitation>): void;
25
- set(lockHolder: FlowLockHolder | null, newState: Invitation.State): boolean;
26
- error(lockHolder: FlowLockHolder | null, error: any): boolean;
27
- }
28
-
29
- export const createGuardedInvitationState = (
30
- ctx: Context,
31
- invitation: Invitation,
32
- stream: PushStream<Invitation>,
33
- ): GuardedInvitationState => {
34
- // the mutex guards invitation flow on host and guest side, making sure only one flow is currently active
35
- // deadlocks seem very unlikely because hosts don't initiate multiple connections
36
- // even if this somehow happens that there are 2 guests (A, B) and 2 hosts (1, 2) and:
37
- // A has lock for flow with 1, B has lock for flow with 2
38
- // 1 has lock for flow with B, 2 has lock for flow with A
39
- // there'll be a 10-second introduction timeout after which connection will be closed and deadlock broken
40
- const mutex = new Mutex();
41
- let lastActiveLockHolder: FlowLockHolder | null = null;
42
- let currentInvitation = { ...invitation };
43
- const isStateChangeAllowed = (lockHolder: FlowLockHolder | null) => {
44
- if (ctx.disposed || (lockHolder !== null && mutex.isLocked() && !lockHolder.hasFlowLock())) {
45
- return false;
46
- }
47
- // don't allow transitions from a terminal state unless a new extension acquired mutex
48
- // handles a case when error occurs (e.g. connection is closed) after we completed the flow
49
- // successfully or already reported another error
50
- return lockHolder == null || lastActiveLockHolder !== lockHolder || isNonTerminalState(currentInvitation.state);
51
- };
52
- return {
53
- mutex,
54
- get current() {
55
- return currentInvitation;
56
- },
57
- // disposing context prevents any further state updates
58
- complete: (newState: Partial<Invitation>) => {
59
- currentInvitation = { ...currentInvitation, ...newState };
60
- stream.next(currentInvitation);
61
- return ctx.dispose();
62
- },
63
- set: (lockHolder: FlowLockHolder | null, newState: Invitation.State): boolean => {
64
- if (isStateChangeAllowed(lockHolder)) {
65
- logStateUpdate(currentInvitation, lockHolder, newState);
66
- currentInvitation = { ...currentInvitation, state: newState };
67
- stream.next(currentInvitation);
68
- lastActiveLockHolder = lockHolder;
69
- return true;
70
- }
71
- return false;
72
- },
73
- error: (lockHolder: FlowLockHolder | null, error: any): boolean => {
74
- if (isStateChangeAllowed(lockHolder)) {
75
- logStateUpdate(currentInvitation, lockHolder, Invitation.State.ERROR);
76
- currentInvitation = { ...currentInvitation, state: Invitation.State.ERROR };
77
- stream.next(currentInvitation);
78
- stream.error(error);
79
- lastActiveLockHolder = lockHolder;
80
- return true;
81
- }
82
- return false;
83
- },
84
- };
85
- };
86
-
87
- const logStateUpdate = (invitation: Invitation, actor: any, newState: Invitation.State) => {
88
- if (isNonTerminalState(newState)) {
89
- log('invitation state update', {
90
- actor: actor?.constructor.name,
91
- newState: stateToString(newState),
92
- oldState: stateToString(invitation.state),
93
- });
94
- } else {
95
- log.info('invitation state update', {
96
- actor: actor?.constructor.name,
97
- newState: stateToString(newState),
98
- oldState: stateToString(invitation.state),
99
- });
100
- }
101
- };
102
-
103
- const isNonTerminalState = (currentState: Invitation.State): boolean => {
104
- return ![
105
- Invitation.State.SUCCESS,
106
- Invitation.State.ERROR,
107
- Invitation.State.CANCELLED,
108
- Invitation.State.TIMEOUT,
109
- Invitation.State.EXPIRED,
110
- ].includes(currentState);
111
- };
@@ -1,253 +0,0 @@
1
- //
2
- // Copyright 2022 DXOS.org
3
- //
4
-
5
- import { decode as decodeCbor, encode as encodeCbor } from 'cbor-x';
6
- import { getRandomPort } from 'get-port-please';
7
- import { describe, test, onTestFinished, vi, expect } from 'vitest';
8
-
9
- import { Trigger, sleep } from '@dxos/async';
10
- import { Context } from '@dxos/context';
11
- import { valueEncoding } from '@dxos/echo-pipeline';
12
- import { createEphemeralEdgeIdentity, EdgeClient, EdgeIdentityChangedError } from '@dxos/edge-client';
13
- import { createTestEdgeWsServer } from '@dxos/edge-client/testing';
14
- import { FeedFactory, FeedStore } from '@dxos/feed-store';
15
- import { type FeedWrapper } from '@dxos/feed-store';
16
- import { Keyring } from '@dxos/keyring';
17
- import { SpaceId } from '@dxos/keys';
18
- import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
19
- import { createStorage } from '@dxos/random-access-storage';
20
- import { openAndClose } from '@dxos/test-utils';
21
- import { Timeframe } from '@dxos/timeframe';
22
- import { range } from '@dxos/util';
23
-
24
- import { EdgeFeedReplicator } from './edge-feed-replicator';
25
-
26
- describe('EdgeFeedReplicator', () => {
27
- test('requests metadata after connection is open', async () => {
28
- const { endpoint, admitConnection, messageSink } = await createEdge();
29
- const { messenger, sendSpy } = await createClient(endpoint);
30
-
31
- await attachReplicator(messenger);
32
-
33
- await sleep(50);
34
-
35
- expect(sendSpy).not.toHaveBeenCalled();
36
- expect(messageSink.length).toEqual(0);
37
-
38
- admitConnection.wake();
39
- await expect.poll(() => sendSpy.mock.calls.length).toEqual(1);
40
- expect(messageSink.length).toEqual(1);
41
- expect(messageSink[0].type).toEqual('get-metadata');
42
- });
43
-
44
- test('replicates if added to a connected client', async () => {
45
- const { endpoint, admitConnection, messageSink } = await createEdge();
46
- const { messenger } = await createClient(endpoint);
47
- admitConnection.wake();
48
- await expect.poll(() => messenger.isConnected).toBeTruthy();
49
-
50
- await attachReplicator(messenger);
51
- await expect.poll(() => messageSink.length).toEqual(1);
52
- });
53
-
54
- test('sends a block', async () => {
55
- const { endpoint, admitConnection, messageSink } = await createEdge();
56
- const { messenger } = await createClient(endpoint);
57
-
58
- const { feed } = await attachReplicator(messenger);
59
-
60
- admitConnection.wake();
61
- await appendMessage(feed);
62
-
63
- await expect.poll(() => messageSink.length).toEqual(2);
64
- expect(messageSink[1].type).toEqual('data');
65
- });
66
-
67
- test('re-requests metadata on reconnect', async () => {
68
- const { endpoint, admitConnection, messageSink } = await createEdge();
69
- const { messenger } = await createClient(endpoint);
70
-
71
- await attachReplicator(messenger);
72
-
73
- admitConnection.wake();
74
- await expect.poll(() => messageSink.length).toEqual(1);
75
-
76
- await updateIdentity(messenger);
77
- await messenger.reconnect.waitForCount(1);
78
-
79
- await expect.poll(() => messageSink.length).toEqual(2);
80
- expect(messageSink[1].type).toEqual('get-metadata');
81
- });
82
-
83
- test('recovers after query sending failure during identity change', async () => {
84
- const { endpoint, admitConnection, messageSink } = await createEdge();
85
- const { messenger, sendSpy } = await createClient(endpoint);
86
-
87
- await attachReplicator(messenger);
88
-
89
- sendSpy.mockImplementationOnce(() => {
90
- throw new EdgeIdentityChangedError(); // Hard to mock the exact race condition for when this error is thrown
91
- });
92
- admitConnection.wake();
93
-
94
- await expect.poll(() => sendSpy.mock.calls.length).toEqual(1);
95
- expect(messageSink.length).toEqual(0);
96
- await updateIdentity(messenger);
97
-
98
- await expect.poll(() => messageSink.length).toEqual(1);
99
- expect(messageSink[0].type).toEqual('get-metadata');
100
- });
101
-
102
- test('recovers after response sending failure during identity change', async () => {
103
- const { endpoint, admitConnection, messageSink, sendResponseMessage } = await createEdge();
104
- const { messenger, sendSpy } = await createClient(endpoint);
105
-
106
- const { feed } = await attachReplicator(messenger);
107
- await appendMessage(feed);
108
-
109
- sendSpy.mockImplementationOnce(async (request: any) => {
110
- sendResponseMessage(request, encodeCbor({ type: 'metadata', feedKey: feed.key.toHex(), length: 0 }));
111
- return Promise.resolve();
112
- });
113
- sendSpy.mockImplementationOnce(async () => {
114
- throw new EdgeIdentityChangedError();
115
- });
116
- admitConnection.wake();
117
-
118
- await expect.poll(() => sendSpy.mock.calls.length).toEqual(2);
119
- sendSpy.mockRestore();
120
- expect(messageSink.length).toEqual(0);
121
- await updateIdentity(messenger);
122
-
123
- await messenger.reconnect.waitForCount(1);
124
- await expect.poll(() => messageSink.find((msg) => msg.type === 'data')).toBeDefined();
125
- });
126
-
127
- test('propagates errors unrelated to reconnect', async () => {
128
- const { endpoint, admitConnection } = await createEdge();
129
- const { messenger, sendSpy } = await createClient(endpoint);
130
-
131
- const { replicator } = await attachReplicator(messenger, { skipOpen: true });
132
- const raised = new Trigger();
133
- await replicator.open(new Context({ onError: () => raised.wake() }));
134
- onTestFinished(async () => {
135
- await replicator.close();
136
- });
137
-
138
- sendSpy.mockImplementationOnce(() => {
139
- throw new Error();
140
- });
141
- admitConnection.wake();
142
-
143
- await raised.wait();
144
- });
145
-
146
- test('identity update before connected', async () => {
147
- const { endpoint, admitConnection, messageSink } = await createEdge();
148
- const { messenger } = await createClient(endpoint);
149
-
150
- await attachReplicator(messenger);
151
- await updateIdentity(messenger);
152
- await sleep(100);
153
- admitConnection.wake();
154
-
155
- await expect.poll(() => messageSink.length).toEqual(2);
156
- expect(messageSink.map((m) => m.type)).toStrictEqual(range(2, () => 'get-metadata'));
157
- });
158
-
159
- test('block appended during reconnect', async () => {
160
- const { endpoint, admitConnection, feedLength } = await createEdge();
161
- const { messenger } = await createClient(endpoint);
162
-
163
- const { feed } = await attachReplicator(messenger);
164
- admitConnection.wake();
165
- await sleep(10);
166
-
167
- admitConnection.reset();
168
- await updateIdentity(messenger);
169
- await appendMessage(feed);
170
- await sleep(20);
171
- admitConnection.wake();
172
-
173
- await expect.poll(() => feedLength()).toEqual(1);
174
- });
175
-
176
- test('reconnect during block append', async () => {
177
- const { endpoint, admitConnection, feedLength } = await createEdge();
178
- const { messenger } = await createClient(endpoint);
179
-
180
- const { feed } = await attachReplicator(messenger);
181
- admitConnection.wake();
182
- await sleep(10);
183
-
184
- void appendMessage(feed);
185
- await updateIdentity(messenger);
186
-
187
- await expect.poll(() => feedLength()).toEqual(1);
188
- });
189
-
190
- const createEdge = async () => {
191
- const port = await getRandomPort('127.0.0.1');
192
- let lastBlockIndex = -1;
193
- const admitConnection = new Trigger();
194
- const { cleanup, endpoint, messageSink, sendResponseMessage } = await createTestEdgeWsServer(port, {
195
- admitConnection,
196
- payloadDecoder: decodeCbor,
197
- messageHandler: async (message: any) => {
198
- if (message.type === 'get-metadata') {
199
- return encodeCbor({ type: 'metadata', feedKey: message.feedKey, length: lastBlockIndex + 1 });
200
- } else {
201
- lastBlockIndex = Math.max(lastBlockIndex, message.blocks[message.blocks.length - 1].index);
202
- }
203
- },
204
- });
205
- onTestFinished(cleanup);
206
-
207
- return {
208
- endpoint,
209
- messageSink,
210
- admitConnection,
211
- sendResponseMessage,
212
- feedLength: () => lastBlockIndex + 1,
213
- };
214
- };
215
-
216
- const createClient = async (endpoint: string) => {
217
- const messenger = new EdgeClient(await createEphemeralEdgeIdentity(), { socketEndpoint: endpoint });
218
- const sendSpy = vi.spyOn(messenger, 'send');
219
- await openAndClose(messenger);
220
- return { messenger, sendSpy };
221
- };
222
-
223
- const attachReplicator = async (messenger: EdgeClient, options?: { skipOpen?: boolean }) => {
224
- const spaceId = SpaceId.random();
225
- const feed = await createNewFeed();
226
- const replicator = new EdgeFeedReplicator({ messenger, spaceId });
227
- await replicator.addFeed(feed);
228
- if (!options?.skipOpen) {
229
- await openAndClose(replicator);
230
- }
231
- return { feed, replicator };
232
- };
233
-
234
- const createNewFeed = async () => {
235
- const storage = createStorage();
236
- const keyring = new Keyring();
237
- const feedStore = new FeedStore<FeedMessage>({
238
- factory: new FeedFactory<FeedMessage>({
239
- root: storage.createDirectory(),
240
- signer: keyring,
241
- hypercore: { valueEncoding },
242
- }),
243
- });
244
- onTestFinished(() => feedStore.close());
245
- return feedStore.openFeed(await keyring.createKey(), { writable: true });
246
- };
247
-
248
- const updateIdentity = async (messenger: EdgeClient) => {
249
- messenger.setIdentity(await createEphemeralEdgeIdentity());
250
- };
251
-
252
- const appendMessage = (feed: FeedWrapper<FeedMessage>) => feed.append({ timeframe: new Timeframe() });
253
- });
@@ -1,11 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- import { runTestSignalServer } from '@dxos/signal';
6
-
7
- const port = 12004;
8
-
9
- export default async () => {
10
- await runTestSignalServer({ port, killExisting: true });
11
- };