@automerge/automerge-repo 2.5.2 → 2.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -30,7 +30,7 @@ export { isValidAutomergeUrl, isValidDocumentId, parseAutomergeUrl, stringifyAut
30
30
  export { Repo } from "./Repo.js";
31
31
  export { Presence } from "./presence/Presence.js";
32
32
  export { PeerStateView } from "./presence/PeerStateView.js";
33
- export type { PeerState, PresenceState, PresenceConfig, UserId, DeviceId, } from "./presence/types.js";
33
+ export type { PeerState, PresenceState, PresenceConfig, } from "./presence/types.js";
34
34
  export { NetworkAdapter } from "./network/NetworkAdapter.js";
35
35
  export type { NetworkAdapterInterface } from "./network/NetworkAdapterInterface.js";
36
36
  export { isRepoMessage } from "./network/messages.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { PeerId } from "../types.js";
2
2
  import { PeerStateView } from "./PeerStateView.js";
3
- import { DeviceId, PresenceState, UserId } from "./types.js";
3
+ import { PresenceState } from "./types.js";
4
4
  export declare class PeerPresenceInfo<State extends PresenceState> {
5
5
  #private;
6
6
  readonly ttl: number;
@@ -25,10 +25,8 @@ export declare class PeerPresenceInfo<State extends PresenceState> {
25
25
  * @param peerId
26
26
  * @param value
27
27
  */
28
- update({ peerId, deviceId, userId, value, }: {
28
+ update({ peerId, value }: {
29
29
  peerId: PeerId;
30
- deviceId?: DeviceId;
31
- userId?: UserId;
32
30
  value: Partial<State>;
33
31
  }): void;
34
32
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"PeerPresenceInfo.d.ts","sourceRoot":"","sources":["../../src/presence/PeerPresenceInfo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAE5D,qBAAa,gBAAgB,CAAC,KAAK,SAAS,aAAa;;IAS3C,QAAQ,CAAC,GAAG,EAAE,MAAM;IANhC;;;;;OAKG;gBACkB,GAAG,EAAE,MAAM;IAEhC,GAAG,CAAC,MAAM,EAAE,MAAM;IAIlB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM;IAcvB;;;;;;OAMG;IACH,MAAM,CAAC,EACL,MAAM,EACN,QAAQ,EACR,MAAM,EACN,KAAK,GACN,EAAE;QACD,MAAM,EAAE,MAAM,CAAA;QACd,QAAQ,CAAC,EAAE,QAAQ,CAAA;QACnB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;KACtB;IAoBD;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM;IAUrB;;;OAGG;IACH,KAAK;IAiBL;;OAEG;IACH,IAAI,MAAM,yBAET;CACF"}
1
+ {"version":3,"file":"PeerPresenceInfo.d.ts","sourceRoot":"","sources":["../../src/presence/PeerPresenceInfo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE1C,qBAAa,gBAAgB,CAAC,KAAK,SAAS,aAAa;;IAS3C,QAAQ,CAAC,GAAG,EAAE,MAAM;IANhC;;;;;OAKG;gBACkB,GAAG,EAAE,MAAM;IAEhC,GAAG,CAAC,MAAM,EAAE,MAAM;IAIlB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM;IAcvB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;KAAE;IAkBnE;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM;IAUrB;;;OAGG;IACH,KAAK;IAiBL;;OAEG;IACH,IAAI,MAAM,yBAET;CACF"}
@@ -39,7 +39,7 @@ export class PeerPresenceInfo {
39
39
  * @param peerId
40
40
  * @param value
41
41
  */
42
- update({ peerId, deviceId, userId, value, }) {
42
+ update({ peerId, value }) {
43
43
  const peerState = this.#peerStates.value[peerId];
44
44
  const existingState = peerState?.value ?? {};
45
45
  const now = Date.now();
@@ -47,10 +47,8 @@ export class PeerPresenceInfo {
47
47
  ...this.#peerStates.value,
48
48
  [peerId]: {
49
49
  peerId,
50
- deviceId,
51
- userId,
52
- lastActiveAt: now,
53
50
  lastSeenAt: now,
51
+ lastActiveAt: now,
54
52
  value: {
55
53
  ...existingState,
56
54
  ...value,
@@ -1,69 +1,60 @@
1
- import { PeerId } from "../types.js";
2
- import { DeviceId, PeerStatesValue, PresenceState, UserId } from "./types.js";
3
- export declare class PeerStateView<State extends PresenceState> {
4
- readonly value: PeerStatesValue<State>;
5
- constructor(value: PeerStatesValue<State>);
6
- /**
7
- * Get all users.
8
- *
9
- * @returns Array of user presence {@link State}s
10
- */
11
- get users(): (import("./types.js").PeerState<State> | undefined)[];
1
+ import { PeerState, PeerStatesValue, PresenceState } from "./types.js";
2
+ export type GetStatesOpts<State extends PresenceState, SummaryState> = {
12
3
  /**
13
- * Get all devices.
4
+ * Function to derive a grouping key from a peer state. This can be used to
5
+ * group peers and consider presence activity by an arbitrary attribute of the
6
+ * presence state (e.g., user or device) rather than by peer.
14
7
  *
15
- * @returns Array of device presence {@link State}s
16
- */
17
- get devices(): (import("./types.js").PeerState<State> | undefined)[];
18
- /**
19
- * Get all peers.
20
- *
21
- * @returns Array of peer presence {@link State}s
22
- */
23
- get peers(): import("./types.js").PeerState<State>[];
24
- /**
25
- * Get all peer ids for this user.
8
+ * This is useful when a user has multiple devices, or multiple peers (e.g.,
9
+ * tabs) on a single device.
26
10
  *
27
- * @param userId
28
- * @returns Array of peer ids for this user
11
+ * @param state state of a peer
12
+ * @returns key that should be used to consolidate activity from that peer
29
13
  */
30
- getUserPeers(userId: UserId): PeerId[];
14
+ groupingFn?: (state: PeerState<State>) => PropertyKey;
31
15
  /**
32
- * Get all peers for this device.
16
+ * Function to summarize the presence activity from several different peers in
17
+ * a group.
33
18
  *
34
- * @param deviceId
35
- * @returns Array of peer ids for this device
19
+ * @param states states of all peers in a group, as grouped by {@param keyFn}
20
+ * @returns a value summarizing presence for this group
36
21
  */
37
- getDevicePeers(deviceId: DeviceId): PeerId[];
38
- /**
39
- * Return the most-recently-seen peer from this group.
40
- *
41
- * @param peers
42
- * @returns id of most recently seen peer
43
- */
44
- getLastSeenPeer(peers: PeerId[]): PeerId | undefined;
45
- /**
46
- * Return the peer from this group that sent a state update most recently
47
- *
48
- * @param peers
49
- * @returns id of most recently seen peer
50
- */
51
- getLastActivePeer(peers: PeerId[]): PeerId | undefined;
52
- /**
53
- * Get current ephemeral state value for this user's most-recently-active
54
- * peer.
55
- *
56
- * @param userId
57
- * @returns user's {@link State}
58
- */
59
- getUserState(userId: UserId): import("./types.js").PeerState<State> | undefined;
22
+ summaryFn?: (states: PeerState<State>[]) => SummaryState;
23
+ };
24
+ /**
25
+ * A grouped view of peer states.
26
+ */
27
+ export declare class PeerStateView<State extends PresenceState> {
28
+ readonly value: PeerStatesValue<State>;
29
+ constructor(value: PeerStatesValue<State>);
60
30
  /**
61
- * Get current ephemeral state value for this device's most-recently-active
62
- * peer.
31
+ * Get the presence state of all peers. By default, each peer is its own
32
+ * group, but presence activity can be aggregated by arbitrary criteria.
63
33
  *
64
- * @param deviceId
65
- * @returns device's {@link State}
34
+ * @param opts
35
+ * @returns presence state for all groups
66
36
  */
67
- getDeviceState(deviceId: DeviceId): import("./types.js").PeerState<State> | undefined;
37
+ getStates<SummaryState = PeerState<State>>(opts?: GetStatesOpts<State, SummaryState>): Record<PropertyKey, SummaryState>;
68
38
  }
39
+ /**
40
+ * Get the peerId of this peer.
41
+ *
42
+ * @param peer
43
+ * @returns peer id
44
+ */
45
+ export declare function peerIdentity<State extends PresenceState>(peer: PeerState<State>): import("../types.js").PeerId;
46
+ /**
47
+ * Find the peer that most recently sent a state update.
48
+ *
49
+ * @param peers
50
+ * @returns id of most recently active peer
51
+ */
52
+ export declare function getLastActivePeer<State extends PresenceState>(peers: PeerState<State>[]): PeerState<State> | undefined;
53
+ /**
54
+ * Find the peer that most recently sent a heartbeat.
55
+ *
56
+ * @param peers
57
+ * @returns id of most recently seen peer
58
+ */
59
+ export declare function getLastSeenPeer<State extends PresenceState>(peers: PeerState<State>[]): PeerState<State> | undefined;
69
60
  //# sourceMappingURL=PeerStateView.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PeerStateView.d.ts","sourceRoot":"","sources":["../../src/presence/PeerStateView.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAE7E,qBAAa,aAAa,CAAC,KAAK,SAAS,aAAa;IACpD,QAAQ,CAAC,KAAK,yBAAA;gBAEF,KAAK,EAAE,eAAe,CAAC,KAAK,CAAC;IAIzC;;;;OAIG;IACH,IAAI,KAAK,0DAKR;IAED;;;;OAIG;IACH,IAAI,OAAO,0DAKV;IAED;;;;OAIG;IACH,IAAI,KAAK,4CAER;IAED;;;;;OAKG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM;IAM3B;;;;;OAKG;IACH,cAAc,CAAC,QAAQ,EAAE,QAAQ;IAMjC;;;;;OAKG;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE;IAiB/B;;;;;OAKG;IACH,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE;IAiBjC;;;;;;OAMG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM;IAa3B;;;;;;OAMG;IACH,cAAc,CAAC,QAAQ,EAAE,QAAQ;CAYlC"}
1
+ {"version":3,"file":"PeerStateView.d.ts","sourceRoot":"","sources":["../../src/presence/PeerStateView.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAEtE,MAAM,MAAM,aAAa,CAAC,KAAK,SAAS,aAAa,EAAE,YAAY,IAAI;IACrE;;;;;;;;;;OAUG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,KAAK,CAAC,KAAK,WAAW,CAAA;IACrD;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,KAAK,YAAY,CAAA;CACzD,CAAA;AAED;;GAEG;AACH,qBAAa,aAAa,CAAC,KAAK,SAAS,aAAa;IACpD,QAAQ,CAAC,KAAK,yBAAA;gBAEF,KAAK,EAAE,eAAe,CAAC,KAAK,CAAC;IAIzC;;;;;;OAMG;IACH,SAAS,CAAC,YAAY,GAAG,SAAS,CAAC,KAAK,CAAC,EACvC,IAAI,CAAC,EAAE,aAAa,CAAC,KAAK,EAAE,YAAY,CAAC;CAoB5C;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,SAAS,aAAa,EACtD,IAAI,EAAE,SAAS,CAAC,KAAK,CAAC,gCAGvB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,SAAS,aAAa,EAC3D,KAAK,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,gCAgB1B;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,SAAS,aAAa,EACzD,KAAK,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,gCAgB1B"}
@@ -1,131 +1,82 @@
1
- import { unique } from "../helpers/array.js";
1
+ /**
2
+ * A grouped view of peer states.
3
+ */
2
4
  export class PeerStateView {
3
5
  value;
4
6
  constructor(value) {
5
7
  this.value = value;
6
8
  }
7
9
  /**
8
- * Get all users.
10
+ * Get the presence state of all peers. By default, each peer is its own
11
+ * group, but presence activity can be aggregated by arbitrary criteria.
9
12
  *
10
- * @returns Array of user presence {@link State}s
13
+ * @param opts
14
+ * @returns presence state for all groups
11
15
  */
12
- get users() {
13
- const userIds = unique(Object.values(this.value).map(peerState => peerState.userId));
14
- return userIds.map(u => this.getUserState(u));
15
- }
16
- /**
17
- * Get all devices.
18
- *
19
- * @returns Array of device presence {@link State}s
20
- */
21
- get devices() {
22
- const deviceIds = unique(Object.values(this.value).map(peerState => peerState.deviceId));
23
- return deviceIds.map(d => this.getDeviceState(d));
24
- }
25
- /**
26
- * Get all peers.
27
- *
28
- * @returns Array of peer presence {@link State}s
29
- */
30
- get peers() {
31
- return Object.values(this.value);
32
- }
33
- /**
34
- * Get all peer ids for this user.
35
- *
36
- * @param userId
37
- * @returns Array of peer ids for this user
38
- */
39
- getUserPeers(userId) {
40
- return Object.values(this.value)
41
- .filter(peerState => peerState.userId === userId)
42
- .map(peerState => peerState.peerId);
43
- }
44
- /**
45
- * Get all peers for this device.
46
- *
47
- * @param deviceId
48
- * @returns Array of peer ids for this device
49
- */
50
- getDevicePeers(deviceId) {
51
- return Object.values(this.value)
52
- .filter(peerState => peerState.deviceId === deviceId)
53
- .map(peerState => peerState.peerId);
54
- }
55
- /**
56
- * Return the most-recently-seen peer from this group.
57
- *
58
- * @param peers
59
- * @returns id of most recently seen peer
60
- */
61
- getLastSeenPeer(peers) {
62
- let freshestLastSeenAt;
63
- return peers.reduce((freshest, curr) => {
64
- const lastSeenAt = this.value[curr]?.lastSeenAt;
65
- if (!lastSeenAt) {
66
- return freshest;
16
+ getStates(opts) {
17
+ const groupingFn = opts?.groupingFn ?? peerIdentity;
18
+ const summaryFn = opts?.summaryFn ??
19
+ getLastActivePeer;
20
+ const statesByKey = Object.values(this.value).reduce((byKey, curr) => {
21
+ const key = groupingFn(curr);
22
+ if (!(key in byKey)) {
23
+ byKey[key] = [];
67
24
  }
68
- if (!freshest || lastSeenAt > freshestLastSeenAt) {
69
- freshestLastSeenAt = lastSeenAt;
70
- return curr;
71
- }
72
- return freshest;
73
- }, undefined);
25
+ byKey[key].push(curr);
26
+ return byKey;
27
+ }, {});
28
+ return Object.entries(statesByKey).reduce((result, [key, states]) => {
29
+ result[key] = summaryFn(states);
30
+ return result;
31
+ }, {});
74
32
  }
75
- /**
76
- * Return the peer from this group that sent a state update most recently
77
- *
78
- * @param peers
79
- * @returns id of most recently seen peer
80
- */
81
- getLastActivePeer(peers) {
82
- let freshestLastActiveAt;
83
- return peers.reduce((freshest, curr) => {
84
- const lastActiveAt = this.value[curr]?.lastActiveAt;
85
- if (!lastActiveAt) {
86
- return freshest;
87
- }
88
- if (!freshest || lastActiveAt > freshestLastActiveAt) {
89
- freshestLastActiveAt = lastActiveAt;
90
- return curr;
91
- }
33
+ }
34
+ /**
35
+ * Get the peerId of this peer.
36
+ *
37
+ * @param peer
38
+ * @returns peer id
39
+ */
40
+ export function peerIdentity(peer) {
41
+ return peer.peerId;
42
+ }
43
+ /**
44
+ * Find the peer that most recently sent a state update.
45
+ *
46
+ * @param peers
47
+ * @returns id of most recently active peer
48
+ */
49
+ export function getLastActivePeer(peers) {
50
+ let freshestLastActiveAt;
51
+ return peers.reduce((freshest, curr) => {
52
+ const lastActiveAt = curr.lastActiveAt;
53
+ if (!lastActiveAt) {
92
54
  return freshest;
93
- }, undefined);
94
- }
95
- /**
96
- * Get current ephemeral state value for this user's most-recently-active
97
- * peer.
98
- *
99
- * @param userId
100
- * @returns user's {@link State}
101
- */
102
- getUserState(userId) {
103
- const peers = this.getUserPeers(userId);
104
- if (!peers) {
105
- return undefined;
106
55
  }
107
- const peer = this.getLastActivePeer(peers);
108
- if (!peer) {
109
- return undefined;
56
+ if (!freshest || lastActiveAt > freshestLastActiveAt) {
57
+ freshestLastActiveAt = lastActiveAt;
58
+ return curr;
110
59
  }
111
- return this.value[peer];
112
- }
113
- /**
114
- * Get current ephemeral state value for this device's most-recently-active
115
- * peer.
116
- *
117
- * @param deviceId
118
- * @returns device's {@link State}
119
- */
120
- getDeviceState(deviceId) {
121
- const peers = this.getDevicePeers(deviceId);
122
- if (!peers) {
123
- return undefined;
60
+ return freshest;
61
+ }, undefined);
62
+ }
63
+ /**
64
+ * Find the peer that most recently sent a heartbeat.
65
+ *
66
+ * @param peers
67
+ * @returns id of most recently seen peer
68
+ */
69
+ export function getLastSeenPeer(peers) {
70
+ let freshestLastSeenAt;
71
+ return peers.reduce((freshest, curr) => {
72
+ const lastSeenAt = curr.lastSeenAt;
73
+ if (!lastSeenAt) {
74
+ return freshest;
124
75
  }
125
- const peer = this.getLastActivePeer(peers);
126
- if (!peer) {
127
- return undefined;
76
+ if (!freshest || lastSeenAt > freshestLastSeenAt) {
77
+ freshestLastSeenAt = lastSeenAt;
78
+ return curr;
128
79
  }
129
- return this.value[peer];
130
- }
80
+ return freshest;
81
+ }, undefined);
131
82
  }
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from "eventemitter3";
2
2
  import { DocHandle } from "../DocHandle.js";
3
- import { DeviceId, PresenceConfig, PresenceEvents, PresenceState, UserId } from "./types.js";
3
+ import { PresenceConfig, PresenceEvents, PresenceState } from "./types.js";
4
4
  /**
5
5
  * Presence encapsulates ephemeral state communication for a specific doc
6
6
  * handle. It tracks caller-provided local state and broadcasts that state to
@@ -14,20 +14,14 @@ import { DeviceId, PresenceConfig, PresenceEvents, PresenceState, UserId } from
14
14
  */
15
15
  export declare class Presence<State extends PresenceState, DocType = any> extends EventEmitter<PresenceEvents> {
16
16
  #private;
17
- readonly deviceId?: DeviceId;
18
- readonly userId?: UserId;
19
17
  /**
20
18
  * Create a new Presence to share ephemeral state with peers.
21
19
  *
22
20
  * @param config see {@link PresenceConfig}
23
21
  * @returns
24
22
  */
25
- constructor({ handle, deviceId, userId, }: {
23
+ constructor({ handle }: {
26
24
  handle: DocHandle<DocType>;
27
- /** Our device id (like userId, this is unverified; peers can send anything) */
28
- deviceId?: DeviceId;
29
- /** Our user id (this is unverified; peers can send anything) */
30
- userId?: UserId;
31
25
  });
32
26
  /**
33
27
  * Start listening to ephemeral messages on the handle, broadcast initial
@@ -1 +1 @@
1
- {"version":3,"file":"Presence.d.ts","sourceRoot":"","sources":["../../src/presence/Presence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAE5C,OAAO,EAAE,SAAS,EAAoC,MAAM,iBAAiB,CAAA;AAC7E,OAAO,EACL,QAAQ,EACR,cAAc,EACd,cAAc,EAGd,aAAa,EACb,MAAM,EACP,MAAM,YAAY,CAAA;AAQnB;;;;;;;;;;GAUG;AACH,qBAAa,QAAQ,CACnB,KAAK,SAAS,aAAa,EAC3B,OAAO,GAAG,GAAG,CACb,SAAQ,YAAY,CAAC,cAAc,CAAC;;IAEpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAA;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IAexB;;;;;OAKG;gBACS,EACV,MAAM,EACN,QAAQ,EACR,MAAM,GACP,EAAE;QACD,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAA;QAC1B,+EAA+E;QAC/E,QAAQ,CAAC,EAAE,QAAQ,CAAA;QACnB,gEAAgE;QAChE,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB;IASD;;;OAGG;IACH,KAAK,CAAC,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,EAAE,cAAc,CAAC,KAAK,CAAC;IA8ErE;;OAEG;IACH,aAAa;IAIb;;OAEG;IACH,aAAa;IAIb;;;;;;OAMG;IACH,SAAS,CAAC,OAAO,SAAS,MAAM,KAAK,EACnC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC;IAQvB;;;OAGG;IACH,IAAI,OAAO,YAEV;IAED;;;;;;;;;;OAUG;IACH,IAAI;IAeJ,OAAO,CAAC,QAAQ;IAWhB,OAAO,CAAC,mBAAmB;IAK3B,OAAO,CAAC,qBAAqB;IAM7B,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,WAAW;IAYnB,OAAO,CAAC,IAAI;IASZ,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,gBAAgB;CAOzB"}
1
+ {"version":3,"file":"Presence.d.ts","sourceRoot":"","sources":["../../src/presence/Presence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAE5C,OAAO,EAAE,SAAS,EAAoC,MAAM,iBAAiB,CAAA;AAC7E,OAAO,EACL,cAAc,EACd,cAAc,EAGd,aAAa,EACd,MAAM,YAAY,CAAA;AAQnB;;;;;;;;;;GAUG;AACH,qBAAa,QAAQ,CACnB,KAAK,SAAS,aAAa,EAC3B,OAAO,GAAG,GAAG,CACb,SAAQ,YAAY,CAAC,cAAc,CAAC;;IAgBpC;;;;;OAKG;gBACS,EAAE,MAAM,EAAE,EAAE;QAAE,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAA;KAAE;IAOtD;;;OAGG;IACH,KAAK,CAAC,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,EAAE,cAAc,CAAC,KAAK,CAAC;IAqErE;;OAEG;IACH,aAAa;IAIb;;OAEG;IACH,aAAa;IAIb;;;;;;OAMG;IACH,SAAS,CAAC,OAAO,SAAS,MAAM,KAAK,EACnC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC;IAQvB;;;OAGG;IACH,IAAI,OAAO,YAEV;IAED;;;;;;;;;;OAUG;IACH,IAAI;IAeJ,OAAO,CAAC,QAAQ;IAWhB,OAAO,CAAC,mBAAmB;IAK3B,OAAO,CAAC,qBAAqB;IAM7B,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,IAAI;IASZ,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,gBAAgB;CAOzB"}
@@ -14,8 +14,6 @@ import { PeerPresenceInfo } from "./PeerPresenceInfo.js";
14
14
  */
15
15
  export class Presence extends EventEmitter {
16
16
  #handle;
17
- deviceId;
18
- userId;
19
17
  #peers;
20
18
  #localState;
21
19
  #heartbeatMs;
@@ -30,13 +28,11 @@ export class Presence extends EventEmitter {
30
28
  * @param config see {@link PresenceConfig}
31
29
  * @returns
32
30
  */
33
- constructor({ handle, deviceId, userId, }) {
31
+ constructor({ handle }) {
34
32
  super();
35
33
  this.#handle = handle;
36
34
  this.#peers = new PeerPresenceInfo(DEFAULT_PEER_TTL_MS);
37
35
  this.#localState = {};
38
- this.userId = userId;
39
- this.deviceId = deviceId;
40
36
  }
41
37
  /**
42
38
  * Start listening to ephemeral messages on the handle, broadcast initial
@@ -60,7 +56,6 @@ export class Presence extends EventEmitter {
60
56
  return;
61
57
  }
62
58
  const message = envelope[PRESENCE_MESSAGE_MARKER];
63
- const { deviceId, userId } = message;
64
59
  if (!this.#peers.has(peerId)) {
65
60
  this.announce();
66
61
  }
@@ -76,15 +71,11 @@ export class Presence extends EventEmitter {
76
71
  case "update":
77
72
  this.#peers.update({
78
73
  peerId,
79
- deviceId,
80
- userId,
81
74
  value: { [message.channel]: message.value },
82
75
  });
83
76
  this.emit("update", {
84
77
  type: "update",
85
78
  peerId,
86
- deviceId,
87
- userId,
88
79
  channel: message.channel,
89
80
  value: message.value,
90
81
  });
@@ -92,15 +83,11 @@ export class Presence extends EventEmitter {
92
83
  case "snapshot":
93
84
  this.#peers.update({
94
85
  peerId,
95
- deviceId,
96
- userId,
97
86
  value: message.state,
98
87
  });
99
88
  this.emit("snapshot", {
100
89
  type: "snapshot",
101
90
  peerId,
102
- deviceId,
103
- userId,
104
91
  state: message.state,
105
92
  });
106
93
  break;
@@ -195,8 +182,6 @@ export class Presence extends EventEmitter {
195
182
  }
196
183
  doBroadcast(type, extra) {
197
184
  this.send({
198
- userId: this.userId,
199
- deviceId: this.deviceId,
200
185
  type,
201
186
  ...extra,
202
187
  });
@@ -1,34 +1,26 @@
1
1
  import { PeerId } from "../types.js";
2
2
  import { PRESENCE_MESSAGE_MARKER } from "./constants.js";
3
- export type UserId = unknown;
4
- export type DeviceId = unknown;
5
3
  export type PresenceState = Record<string, any>;
6
4
  export type PeerStatesValue<State extends PresenceState> = Record<PeerId, PeerState<State>>;
7
5
  export type PeerState<State extends PresenceState> = {
8
6
  peerId: PeerId;
9
7
  lastActiveAt: number;
10
8
  lastSeenAt: number;
11
- deviceId?: DeviceId;
12
- userId?: UserId;
13
9
  value: State;
14
10
  };
15
- type PresenceMessageBase = {
16
- deviceId?: DeviceId;
17
- userId?: UserId;
18
- };
19
- type PresenceMessageUpdate = PresenceMessageBase & {
11
+ type PresenceMessageUpdate = {
20
12
  type: "update";
21
13
  channel: string;
22
14
  value: any;
23
15
  };
24
- type PresenceMessageSnapshot = PresenceMessageBase & {
16
+ type PresenceMessageSnapshot = {
25
17
  type: "snapshot";
26
18
  state: any;
27
19
  };
28
- type PresenceMessageHeartbeat = PresenceMessageBase & {
20
+ type PresenceMessageHeartbeat = {
29
21
  type: "heartbeat";
30
22
  };
31
- type PresenceMessageGoodbye = PresenceMessageBase & {
23
+ type PresenceMessageGoodbye = {
32
24
  type: "goodbye";
33
25
  };
34
26
  export type PresenceMessage = {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/presence/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/presence/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAA;AAExD,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AAE/C,MAAM,MAAM,eAAe,CAAC,KAAK,SAAS,aAAa,IAAI,MAAM,CAC/D,MAAM,EACN,SAAS,CAAC,KAAK,CAAC,CACjB,CAAA;AAED,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,aAAa,IAAI;IACnD,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,KAAK,CAAA;CACb,CAAA;AAED,KAAK,qBAAqB,GAAG;IAC3B,IAAI,EAAE,QAAQ,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;CACX,CAAA;AAED,KAAK,uBAAuB,GAAG;IAC7B,IAAI,EAAE,UAAU,CAAA;IAChB,KAAK,EAAE,GAAG,CAAA;CACX,CAAA;AAED,KAAK,wBAAwB,GAAG;IAC9B,IAAI,EAAE,WAAW,CAAA;CAClB,CAAA;AAED,KAAK,sBAAsB,GAAG;IAC5B,IAAI,EAAE,SAAS,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,CAAC,uBAAuB,CAAC,EACrB,qBAAqB,GACrB,uBAAuB,GACvB,wBAAwB,GACxB,sBAAsB,CAAA;CAC3B,CAAA;AAED,MAAM,MAAM,mBAAmB,GAC7B,eAAe,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEzD,KAAK,UAAU,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAEpC,MAAM,MAAM,mBAAmB,GAAG,qBAAqB,GAAG,UAAU,CAAA;AACpE,MAAM,MAAM,qBAAqB,GAAG,uBAAuB,GAAG,UAAU,CAAA;AACxE,MAAM,MAAM,sBAAsB,GAAG,wBAAwB,GAAG,UAAU,CAAA;AAC1E,MAAM,MAAM,oBAAoB,GAAG,sBAAsB,GAAG,UAAU,CAAA;AACtE,MAAM,MAAM,oBAAoB,GAAG;IAAE,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAEvD;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B;;OAEG;IACH,MAAM,EAAE,CAAC,CAAC,EAAE,mBAAmB,KAAK,IAAI,CAAA;IACxC;;OAEG;IACH,QAAQ,EAAE,CAAC,CAAC,EAAE,qBAAqB,KAAK,IAAI,CAAA;IAC5C;;OAEG;IACH,SAAS,EAAE,CAAC,CAAC,EAAE,sBAAsB,KAAK,IAAI,CAAA;IAC9C;;OAEG;IACH,OAAO,EAAE,CAAC,CAAC,EAAE,oBAAoB,KAAK,IAAI,CAAA;IAC1C;;OAEG;IACH,OAAO,EAAE,CAAC,CAAC,EAAE,oBAAoB,KAAK,IAAI,CAAA;CAC3C,CAAA;AAED,MAAM,MAAM,cAAc,CAAC,KAAK,SAAS,aAAa,IAAI;IACxD,mDAAmD;IACnD,YAAY,EAAE,KAAK,CAAA;IACnB,wFAAwF;IACxF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,sGAAsG;IACtG,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA"}
@@ -87,7 +87,7 @@ export class CollectionSynchronizer extends Synchronizer {
87
87
  // Record the request so that even if access is denied now, we know that the
88
88
  // peer requested the document so that if the share policy changes we know
89
89
  // to begin syncing with this peer
90
- if (message.type === "request") {
90
+ if (message.type === "request" || message.type === "sync") {
91
91
  if (!this.#hasRequested.has(documentId)) {
92
92
  this.#hasRequested.set(documentId, new Set());
93
93
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automerge/automerge-repo",
3
- "version": "2.5.2",
3
+ "version": "2.5.3",
4
4
  "description": "A repository object to manage a collection of automerge documents",
5
5
  "repository": "https://github.com/automerge/automerge-repo/tree/master/packages/automerge-repo",
6
6
  "author": "Peter van Hardenberg <pvh@pvh.ca>",
@@ -59,5 +59,5 @@
59
59
  "publishConfig": {
60
60
  "access": "public"
61
61
  },
62
- "gitHead": "7719288215977fdc3ceee8396210feddc78c3f5b"
62
+ "gitHead": "082d2134aa296b7a966210bc2ec744096733e273"
63
63
  }
package/src/index.ts CHANGED
@@ -45,8 +45,6 @@ export type {
45
45
  PeerState,
46
46
  PresenceState,
47
47
  PresenceConfig,
48
- UserId,
49
- DeviceId,
50
48
  } from "./presence/types.js"
51
49
 
52
50
  export { NetworkAdapter } from "./network/NetworkAdapter.js"
@@ -1,6 +1,6 @@
1
1
  import { PeerId } from "../types.js"
2
2
  import { PeerStateView } from "./PeerStateView.js"
3
- import { DeviceId, PresenceState, UserId } from "./types.js"
3
+ import { PresenceState } from "./types.js"
4
4
 
5
5
  export class PeerPresenceInfo<State extends PresenceState> {
6
6
  #peerStates = new PeerStateView<State>({})
@@ -43,17 +43,7 @@ export class PeerPresenceInfo<State extends PresenceState> {
43
43
  * @param peerId
44
44
  * @param value
45
45
  */
46
- update({
47
- peerId,
48
- deviceId,
49
- userId,
50
- value,
51
- }: {
52
- peerId: PeerId
53
- deviceId?: DeviceId
54
- userId?: UserId
55
- value: Partial<State>
56
- }) {
46
+ update({ peerId, value }: { peerId: PeerId; value: Partial<State> }) {
57
47
  const peerState = this.#peerStates.value[peerId]
58
48
  const existingState = peerState?.value ?? ({} as State)
59
49
  const now = Date.now()
@@ -61,10 +51,8 @@ export class PeerPresenceInfo<State extends PresenceState> {
61
51
  ...this.#peerStates.value,
62
52
  [peerId]: {
63
53
  peerId,
64
- deviceId,
65
- userId,
66
- lastActiveAt: now,
67
54
  lastSeenAt: now,
55
+ lastActiveAt: now,
68
56
  value: {
69
57
  ...existingState,
70
58
  ...value,
@@ -1,154 +1,126 @@
1
- import { unique } from "../helpers/array.js"
2
- import { PeerId } from "../types.js"
3
- import { DeviceId, PeerStatesValue, PresenceState, UserId } from "./types.js"
4
-
5
- export class PeerStateView<State extends PresenceState> {
6
- readonly value
7
-
8
- constructor(value: PeerStatesValue<State>) {
9
- this.value = value
10
- }
1
+ import { PeerState, PeerStatesValue, PresenceState } from "./types.js"
11
2
 
3
+ export type GetStatesOpts<State extends PresenceState, SummaryState> = {
12
4
  /**
13
- * Get all users.
5
+ * Function to derive a grouping key from a peer state. This can be used to
6
+ * group peers and consider presence activity by an arbitrary attribute of the
7
+ * presence state (e.g., user or device) rather than by peer.
14
8
  *
15
- * @returns Array of user presence {@link State}s
16
- */
17
- get users() {
18
- const userIds = unique(
19
- Object.values(this.value).map(peerState => peerState.userId)
20
- )
21
- return userIds.map(u => this.getUserState(u))
22
- }
23
-
24
- /**
25
- * Get all devices.
9
+ * This is useful when a user has multiple devices, or multiple peers (e.g.,
10
+ * tabs) on a single device.
26
11
  *
27
- * @returns Array of device presence {@link State}s
12
+ * @param state state of a peer
13
+ * @returns key that should be used to consolidate activity from that peer
28
14
  */
29
- get devices() {
30
- const deviceIds = unique(
31
- Object.values(this.value).map(peerState => peerState.deviceId)
32
- )
33
- return deviceIds.map(d => this.getDeviceState(d))
34
- }
35
-
15
+ groupingFn?: (state: PeerState<State>) => PropertyKey
36
16
  /**
37
- * Get all peers.
17
+ * Function to summarize the presence activity from several different peers in
18
+ * a group.
38
19
  *
39
- * @returns Array of peer presence {@link State}s
20
+ * @param states states of all peers in a group, as grouped by {@param keyFn}
21
+ * @returns a value summarizing presence for this group
40
22
  */
41
- get peers() {
42
- return Object.values(this.value)
43
- }
23
+ summaryFn?: (states: PeerState<State>[]) => SummaryState
24
+ }
44
25
 
45
- /**
46
- * Get all peer ids for this user.
47
- *
48
- * @param userId
49
- * @returns Array of peer ids for this user
50
- */
51
- getUserPeers(userId: UserId) {
52
- return Object.values(this.value)
53
- .filter(peerState => peerState.userId === userId)
54
- .map(peerState => peerState.peerId)
55
- }
26
+ /**
27
+ * A grouped view of peer states.
28
+ */
29
+ export class PeerStateView<State extends PresenceState> {
30
+ readonly value
56
31
 
57
- /**
58
- * Get all peers for this device.
59
- *
60
- * @param deviceId
61
- * @returns Array of peer ids for this device
62
- */
63
- getDevicePeers(deviceId: DeviceId) {
64
- return Object.values(this.value)
65
- .filter(peerState => peerState.deviceId === deviceId)
66
- .map(peerState => peerState.peerId)
32
+ constructor(value: PeerStatesValue<State>) {
33
+ this.value = value
67
34
  }
68
35
 
69
36
  /**
70
- * Return the most-recently-seen peer from this group.
37
+ * Get the presence state of all peers. By default, each peer is its own
38
+ * group, but presence activity can be aggregated by arbitrary criteria.
71
39
  *
72
- * @param peers
73
- * @returns id of most recently seen peer
40
+ * @param opts
41
+ * @returns presence state for all groups
74
42
  */
75
- getLastSeenPeer(peers: PeerId[]) {
76
- let freshestLastSeenAt: number
77
- return peers.reduce((freshest: PeerId | undefined, curr) => {
78
- const lastSeenAt = this.value[curr]?.lastSeenAt
79
- if (!lastSeenAt) {
80
- return freshest
43
+ getStates<SummaryState = PeerState<State>>(
44
+ opts?: GetStatesOpts<State, SummaryState>
45
+ ) {
46
+ const groupingFn = opts?.groupingFn ?? peerIdentity
47
+ const summaryFn =
48
+ opts?.summaryFn ??
49
+ (getLastActivePeer as (states: PeerState<State>[]) => SummaryState)
50
+ const statesByKey = Object.values(this.value).reduce((byKey, curr) => {
51
+ const key = groupingFn(curr)
52
+ if (!(key in byKey)) {
53
+ byKey[key] = []
81
54
  }
82
-
83
- if (!freshest || lastSeenAt > freshestLastSeenAt) {
84
- freshestLastSeenAt = lastSeenAt
85
- return curr
86
- }
87
-
88
- return freshest
89
- }, undefined)
55
+ byKey[key].push(curr)
56
+
57
+ return byKey
58
+ }, {} as Record<PropertyKey, PeerState<State>[]>)
59
+ return Object.entries(statesByKey).reduce((result, [key, states]) => {
60
+ result[key] = summaryFn(states)
61
+ return result
62
+ }, {} as Record<PropertyKey, SummaryState>)
90
63
  }
64
+ }
91
65
 
92
- /**
93
- * Return the peer from this group that sent a state update most recently
94
- *
95
- * @param peers
96
- * @returns id of most recently seen peer
97
- */
98
- getLastActivePeer(peers: PeerId[]) {
99
- let freshestLastActiveAt: number
100
- return peers.reduce((freshest: PeerId | undefined, curr) => {
101
- const lastActiveAt = this.value[curr]?.lastActiveAt
102
- if (!lastActiveAt) {
103
- return freshest
104
- }
105
-
106
- if (!freshest || lastActiveAt > freshestLastActiveAt) {
107
- freshestLastActiveAt = lastActiveAt
108
- return curr
109
- }
66
+ /**
67
+ * Get the peerId of this peer.
68
+ *
69
+ * @param peer
70
+ * @returns peer id
71
+ */
72
+ export function peerIdentity<State extends PresenceState>(
73
+ peer: PeerState<State>
74
+ ) {
75
+ return peer.peerId
76
+ }
110
77
 
78
+ /**
79
+ * Find the peer that most recently sent a state update.
80
+ *
81
+ * @param peers
82
+ * @returns id of most recently active peer
83
+ */
84
+ export function getLastActivePeer<State extends PresenceState>(
85
+ peers: PeerState<State>[]
86
+ ) {
87
+ let freshestLastActiveAt: number
88
+ return peers.reduce((freshest, curr) => {
89
+ const lastActiveAt = curr.lastActiveAt
90
+ if (!lastActiveAt) {
111
91
  return freshest
112
- }, undefined)
113
- }
114
-
115
- /**
116
- * Get current ephemeral state value for this user's most-recently-active
117
- * peer.
118
- *
119
- * @param userId
120
- * @returns user's {@link State}
121
- */
122
- getUserState(userId: UserId) {
123
- const peers = this.getUserPeers(userId)
124
- if (!peers) {
125
- return undefined
126
92
  }
127
- const peer = this.getLastActivePeer(peers)
128
- if (!peer) {
129
- return undefined
93
+
94
+ if (!freshest || lastActiveAt > freshestLastActiveAt) {
95
+ freshestLastActiveAt = lastActiveAt
96
+ return curr
130
97
  }
131
98
 
132
- return this.value[peer]
133
- }
99
+ return freshest
100
+ }, undefined as PeerState<State> | undefined)
101
+ }
134
102
 
135
- /**
136
- * Get current ephemeral state value for this device's most-recently-active
137
- * peer.
138
- *
139
- * @param deviceId
140
- * @returns device's {@link State}
141
- */
142
- getDeviceState(deviceId: DeviceId) {
143
- const peers = this.getDevicePeers(deviceId)
144
- if (!peers) {
145
- return undefined
103
+ /**
104
+ * Find the peer that most recently sent a heartbeat.
105
+ *
106
+ * @param peers
107
+ * @returns id of most recently seen peer
108
+ */
109
+ export function getLastSeenPeer<State extends PresenceState>(
110
+ peers: PeerState<State>[]
111
+ ) {
112
+ let freshestLastSeenAt: number
113
+ return peers.reduce((freshest, curr) => {
114
+ const lastSeenAt = curr.lastSeenAt
115
+ if (!lastSeenAt) {
116
+ return freshest
146
117
  }
147
- const peer = this.getLastActivePeer(peers)
148
- if (!peer) {
149
- return undefined
118
+
119
+ if (!freshest || lastSeenAt > freshestLastSeenAt) {
120
+ freshestLastSeenAt = lastSeenAt
121
+ return curr
150
122
  }
151
123
 
152
- return this.value[peer]
153
- }
124
+ return freshest
125
+ }, undefined as PeerState<State> | undefined)
154
126
  }
@@ -2,13 +2,11 @@ import { EventEmitter } from "eventemitter3"
2
2
 
3
3
  import { DocHandle, DocHandleEphemeralMessagePayload } from "../DocHandle.js"
4
4
  import {
5
- DeviceId,
6
5
  PresenceConfig,
7
6
  PresenceEvents,
8
7
  PresenceMessage,
9
8
  PresenceMessageType,
10
9
  PresenceState,
11
- UserId,
12
10
  } from "./types.js"
13
11
  import {
14
12
  DEFAULT_HEARTBEAT_INTERVAL_MS,
@@ -33,8 +31,6 @@ export class Presence<
33
31
  DocType = any
34
32
  > extends EventEmitter<PresenceEvents> {
35
33
  #handle: DocHandle<DocType>
36
- readonly deviceId?: DeviceId
37
- readonly userId?: UserId
38
34
  #peers: PeerPresenceInfo<State>
39
35
  #localState: State
40
36
  #heartbeatMs?: number
@@ -55,23 +51,11 @@ export class Presence<
55
51
  * @param config see {@link PresenceConfig}
56
52
  * @returns
57
53
  */
58
- constructor({
59
- handle,
60
- deviceId,
61
- userId,
62
- }: {
63
- handle: DocHandle<DocType>
64
- /** Our device id (like userId, this is unverified; peers can send anything) */
65
- deviceId?: DeviceId
66
- /** Our user id (this is unverified; peers can send anything) */
67
- userId?: UserId
68
- }) {
54
+ constructor({ handle }: { handle: DocHandle<DocType> }) {
69
55
  super()
70
56
  this.#handle = handle
71
57
  this.#peers = new PeerPresenceInfo<State>(DEFAULT_PEER_TTL_MS)
72
58
  this.#localState = {} as State
73
- this.userId = userId
74
- this.deviceId = deviceId
75
59
  }
76
60
 
77
61
  /**
@@ -102,7 +86,6 @@ export class Presence<
102
86
  }
103
87
 
104
88
  const message = envelope[PRESENCE_MESSAGE_MARKER]
105
- const { deviceId, userId } = message
106
89
 
107
90
  if (!this.#peers.has(peerId)) {
108
91
  this.announce()
@@ -120,15 +103,11 @@ export class Presence<
120
103
  case "update":
121
104
  this.#peers.update({
122
105
  peerId,
123
- deviceId,
124
- userId,
125
106
  value: { [message.channel]: message.value } as Partial<State>,
126
107
  })
127
108
  this.emit("update", {
128
109
  type: "update",
129
110
  peerId,
130
- deviceId,
131
- userId,
132
111
  channel: message.channel,
133
112
  value: message.value,
134
113
  })
@@ -136,15 +115,11 @@ export class Presence<
136
115
  case "snapshot":
137
116
  this.#peers.update({
138
117
  peerId,
139
- deviceId,
140
- userId,
141
118
  value: message.state as State,
142
119
  })
143
120
  this.emit("snapshot", {
144
121
  type: "snapshot",
145
122
  peerId,
146
- deviceId,
147
- userId,
148
123
  state: message.state,
149
124
  })
150
125
  break
@@ -256,8 +231,6 @@ export class Presence<
256
231
  extra?: Record<string, unknown>
257
232
  ) {
258
233
  this.send({
259
- userId: this.userId,
260
- deviceId: this.deviceId,
261
234
  type,
262
235
  ...extra,
263
236
  })
@@ -1,9 +1,6 @@
1
1
  import { PeerId } from "../types.js"
2
2
  import { PRESENCE_MESSAGE_MARKER } from "./constants.js"
3
3
 
4
- export type UserId = unknown
5
- export type DeviceId = unknown
6
-
7
4
  export type PresenceState = Record<string, any>
8
5
 
9
6
  export type PeerStatesValue<State extends PresenceState> = Record<
@@ -15,32 +12,25 @@ export type PeerState<State extends PresenceState> = {
15
12
  peerId: PeerId
16
13
  lastActiveAt: number
17
14
  lastSeenAt: number
18
- deviceId?: DeviceId
19
- userId?: UserId
20
15
  value: State
21
16
  }
22
17
 
23
- type PresenceMessageBase = {
24
- deviceId?: DeviceId
25
- userId?: UserId
26
- }
27
-
28
- type PresenceMessageUpdate = PresenceMessageBase & {
18
+ type PresenceMessageUpdate = {
29
19
  type: "update"
30
20
  channel: string
31
21
  value: any
32
22
  }
33
23
 
34
- type PresenceMessageSnapshot = PresenceMessageBase & {
24
+ type PresenceMessageSnapshot = {
35
25
  type: "snapshot"
36
26
  state: any
37
27
  }
38
28
 
39
- type PresenceMessageHeartbeat = PresenceMessageBase & {
29
+ type PresenceMessageHeartbeat = {
40
30
  type: "heartbeat"
41
31
  }
42
32
 
43
- type PresenceMessageGoodbye = PresenceMessageBase & {
33
+ type PresenceMessageGoodbye = {
44
34
  type: "goodbye"
45
35
  }
46
36
 
@@ -112,7 +112,7 @@ export class CollectionSynchronizer extends Synchronizer {
112
112
  // Record the request so that even if access is denied now, we know that the
113
113
  // peer requested the document so that if the share policy changes we know
114
114
  // to begin syncing with this peer
115
- if (message.type === "request") {
115
+ if (message.type === "request" || message.type === "sync") {
116
116
  if (!this.#hasRequested.has(documentId)) {
117
117
  this.#hasRequested.set(documentId, new Set())
118
118
  }
@@ -31,15 +31,11 @@ describe("Presence", () => {
31
31
  })
32
32
  const alicePresence = new Presence<PresenceState>({
33
33
  handle: aliceHandle,
34
- userId: "alice",
35
- deviceId: "phone",
36
34
  })
37
35
 
38
36
  const bobHandle = await bob.find(aliceHandle.url)
39
37
  const bobPresence = new Presence<PresenceState>({
40
38
  handle: bobHandle,
41
- userId: "bob",
42
- deviceId: "phone",
43
39
  })
44
40
 
45
41
  return {
package/test/Repo.test.ts CHANGED
@@ -1247,6 +1247,98 @@ describe("Repo", () => {
1247
1247
  assert.deepStrictEqual(bobHandle.doc(), { foo: "bar" })
1248
1248
  })
1249
1249
 
1250
+ it("sync messages (not just requests) are recorded as peer interest for later share policy changes", async () => {
1251
+ // This test covers a scenario where two peers sync via a sync server.
1252
+ // Initially the sync server allows peerA but denies peerB. PeerB already
1253
+ // has the document (obtained directly from peerA), so when it connects to
1254
+ // the server it sends a "sync" message (not a "request"). The server
1255
+ // denies access. Later the server's share policy changes to allow peerB.
1256
+ // The server should then start pushing updates to peerB. Before the fix
1257
+ // in CollectionSynchronizer, only "request" messages were recorded as
1258
+ // expressing peer interest in a document. So when the share policy
1259
+ // changed, the server had no record that peerB wanted the document and
1260
+ // would not begin syncing — peerB would only receive updates after it
1261
+ // sent another sync message of its own (i.e. after making a write).
1262
+
1263
+ // Track whether peerB is allowed to sync with the server
1264
+ let peerBAllowed = false
1265
+
1266
+ const peerA = new Repo({
1267
+ peerId: "peerA" as PeerId,
1268
+ shareConfig: {
1269
+ announce: async () => true,
1270
+ access: async () => true,
1271
+ },
1272
+ })
1273
+
1274
+ const syncServer = new Repo({
1275
+ peerId: "syncServer" as PeerId,
1276
+ shareConfig: {
1277
+ // Server mode: never proactively announces docs to peers
1278
+ announce: async () => false,
1279
+ // Initially only peerA is allowed; peerB is denied
1280
+ access: async peerId => peerId === "peerA" || peerBAllowed,
1281
+ },
1282
+ })
1283
+
1284
+ const peerB = new Repo({
1285
+ peerId: "peerB" as PeerId,
1286
+ shareConfig: {
1287
+ announce: async () => true,
1288
+ access: async () => true,
1289
+ },
1290
+ })
1291
+
1292
+ // Connect peerA to syncServer so the server gets the document
1293
+ await connectRepos(peerA, syncServer)
1294
+
1295
+ // Create the document on peerA and sync it to the server
1296
+ const handle = peerA.create<TestDoc>({ foo: "bar" })
1297
+ await pause(50) // allow sync to propagate to server
1298
+
1299
+ // Connect peerA to peerB directly so peerB can obtain the document
1300
+ const peerABConnection = await connectRepos(peerA, peerB)
1301
+ const peerBHandle = await withTimeout(
1302
+ peerB.find<TestDoc>(handle.url),
1303
+ 500
1304
+ )
1305
+ assert.ok(
1306
+ peerBHandle,
1307
+ "peerB should have obtained the document from peerA"
1308
+ )
1309
+ assert.deepStrictEqual(peerBHandle!.doc(), { foo: "bar" })
1310
+
1311
+ // Connect peerB to the server. PeerB already has the document so it sends
1312
+ // a "sync" message (not a "request"). The server denies access and —
1313
+ // without the fix — does NOT record peerB's interest.
1314
+ await connectRepos(peerB, syncServer)
1315
+
1316
+ // Sever the direct peerA <-> peerB connection so the only path between
1317
+ // them is through the sync server
1318
+ peerABConnection.disconnect()
1319
+ await pause(10)
1320
+
1321
+ // Now allow peerB on the server and notify it of the policy change
1322
+ peerBAllowed = true
1323
+ syncServer.shareConfigChanged()
1324
+ await pause(50) // allow reevaluateDocumentShare to run
1325
+
1326
+ // peerA makes a new change
1327
+ handle.change(d => {
1328
+ d.foo = "baz"
1329
+ })
1330
+ await pause(150) // allow time for the change to flow peerA → server → peerB
1331
+
1332
+ // With the fix: the server recorded peerB's sync message as expressing
1333
+ // interest, so reevaluateDocumentShare started syncing with peerB, and
1334
+ // peerA's new change flows through the server to peerB.
1335
+ assert.deepStrictEqual(
1336
+ peerBHandle!.doc(),
1337
+ { foo: "baz" },
1338
+ "peerB should have received peerA's change via the sync server after the share policy changed"
1339
+ )
1340
+ })
1341
+
1250
1342
  it("a previously unavailable document becomes available if the network adapter initially has no peers", async () => {
1251
1343
  // It is possible for a network adapter to be ready without any peer
1252
1344
  // being announced (e.g. the BroadcastChannelNetworkAdapter). In this
@@ -1,8 +1,17 @@
1
1
  import { DummyNetworkAdapter } from "../../src/helpers/DummyNetworkAdapter.js"
2
2
  import { Repo } from "../../src/Repo.js"
3
+ import { PeerId } from "../../src/types.js"
3
4
  import pause from "./pause.js"
4
5
 
5
- export default async function connectRepos(left: Repo, right: Repo) {
6
+ export type Connection = {
7
+ disconnect: () => void
8
+ reconnect: () => Promise<void>
9
+ }
10
+
11
+ export default async function connectRepos(
12
+ left: Repo,
13
+ right: Repo
14
+ ): Promise<Connection> {
6
15
  const [leftToRight, rightToLeft] = DummyNetworkAdapter.createConnectedPair({
7
16
  latency: 0,
8
17
  })
@@ -15,4 +24,20 @@ export default async function connectRepos(left: Repo, right: Repo) {
15
24
  right.networkSubsystem.whenReady(),
16
25
  ])
17
26
  await pause(10)
27
+
28
+ return {
29
+ disconnect: () => {
30
+ leftToRight.emit("peer-disconnected", { peerId: right.peerId as PeerId })
31
+ rightToLeft.emit("peer-disconnected", { peerId: left.peerId as PeerId })
32
+ },
33
+ reconnect: async () => {
34
+ leftToRight.peerCandidate(right.peerId)
35
+ rightToLeft.peerCandidate(left.peerId)
36
+ await Promise.all([
37
+ left.networkSubsystem.whenReady(),
38
+ right.networkSubsystem.whenReady(),
39
+ ])
40
+ await pause(10)
41
+ },
42
+ }
18
43
  }
@@ -1,2 +0,0 @@
1
- export declare function unique<T>(items: T[]): T[];
2
- //# sourceMappingURL=array.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"array.d.ts","sourceRoot":"","sources":["../../src/helpers/array.ts"],"names":[],"mappings":"AAAA,wBAAgB,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAEnC"}
@@ -1,3 +0,0 @@
1
- export function unique(items) {
2
- return Array.from(new Set(items));
3
- }
@@ -1,3 +0,0 @@
1
- export function unique<T>(items: T[]) {
2
- return Array.from(new Set(items))
3
- }