@verdant-web/store 3.10.0 → 3.11.1

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.
@@ -172,7 +172,9 @@ export class PresenceManager<
172
172
  this.emit('peerLeft', message.userId, lastPresence);
173
173
  }
174
174
  if (peersChanged) {
175
- this._peerIds = Array.from(peerIdsSet);
175
+ // important that this stays sorted to keep stable order, don't want
176
+ // peers swapping around.
177
+ this._peerIds = Array.from(peerIdsSet).sort();
176
178
  this.emit('peersChanged', this._peers);
177
179
  }
178
180
  if (peersChanged || selfChanged) {
@@ -220,6 +222,17 @@ export class PresenceManager<
220
222
  this.emit('change');
221
223
  };
222
224
 
225
+ setFieldId = (fieldId: string | undefined, timestamp = Date.now()) => {
226
+ this._updateBatch.update({
227
+ items: [
228
+ { internal: { lastFieldId: fieldId, lastFieldTimestamp: timestamp } },
229
+ ],
230
+ });
231
+ this.self.internal.lastFieldId = fieldId;
232
+ this.emit('selfChanged', this.self);
233
+ this.emit('change');
234
+ };
235
+
223
236
  /**
224
237
  * Get all peers that are in the same view as the local user.
225
238
  */
@@ -236,4 +249,18 @@ export class PresenceManager<
236
249
  )
237
250
  );
238
251
  };
252
+
253
+ /**
254
+ * Get all peers that have interacted with the specified
255
+ * field most recently.
256
+ */
257
+ getFieldPeers = (fieldId: string, expirationPeriod = 60 * 1000) => {
258
+ return this._peerIds
259
+ .map((id) => this._peers[id])
260
+ .filter(
261
+ (peer) =>
262
+ peer.internal.lastFieldId === fieldId &&
263
+ Date.now() - peer.internal.lastFieldTimestamp! < expirationPeriod,
264
+ );
265
+ };
239
266
  }
package/src/sync/Sync.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  } from './ServerSyncEndpointProvider.js';
21
21
  import { WebSocketSync } from './WebSocketSync.js';
22
22
  import { Context } from '../context.js';
23
+ import { attemptToRegisterBackgroundSync } from './background.js';
23
24
 
24
25
  type SyncEvents = {
25
26
  onlineChange: (isOnline: boolean) => void;
@@ -184,6 +185,8 @@ export interface ServerSyncOptions<Profile = any, Presence = any>
184
185
  * Not sure why you want to do this, but be careful.
185
186
  */
186
187
  onOutgoingMessage?: (message: ClientMessage) => void;
188
+
189
+ EXPERIMENTAL_backgroundSync?: boolean;
187
190
  }
188
191
 
189
192
  export class ServerSync<Presence = any, Profile = any>
@@ -225,6 +228,7 @@ export class ServerSync<Presence = any, Profile = any>
225
228
  defaultProfile,
226
229
  useBroadcastChannel,
227
230
  onOutgoingMessage,
231
+ EXPERIMENTAL_backgroundSync,
228
232
  }: ServerSyncOptions<Profile, Presence>,
229
233
  {
230
234
  meta,
@@ -332,6 +336,10 @@ export class ServerSync<Presence = any, Profile = any>
332
336
  if (autoStart) {
333
337
  this.start();
334
338
  }
339
+
340
+ if (EXPERIMENTAL_backgroundSync) {
341
+ attemptToRegisterBackgroundSync();
342
+ }
335
343
  }
336
344
 
337
345
  get canDoRealtime() {
@@ -0,0 +1,27 @@
1
+ export async function attemptToRegisterBackgroundSync() {
2
+ try {
3
+ const status = await navigator.permissions.query({
4
+ name: 'periodic-background-sync' as any,
5
+ });
6
+ if (status.state === 'granted') {
7
+ // Periodic background sync can be used.
8
+ const registration = await navigator.serviceWorker.ready;
9
+ if ('periodicSync' in registration) {
10
+ try {
11
+ await (registration.periodicSync as any).register('verdant-sync', {
12
+ // An interval of one day.
13
+ minInterval: 24 * 60 * 60 * 1000,
14
+ });
15
+ } catch (error) {
16
+ // Periodic background sync cannot be used.
17
+ console.warn('Failed to register background sync:', error);
18
+ }
19
+ }
20
+ } else {
21
+ // Periodic background sync cannot be used.
22
+ console.debug('Background sync permission is not granted:', status);
23
+ }
24
+ } catch (error) {
25
+ console.error('Failed to initiate background sync:', error);
26
+ }
27
+ }
@@ -0,0 +1,18 @@
1
+ export function cliSync<Presence extends any = any>(
2
+ libraryId: string,
3
+ {
4
+ port = 3242,
5
+ initialPresence = {} as any,
6
+ }: { port?: number; initialPresence?: Presence } = {},
7
+ ) {
8
+ let userId = localStorage.getItem('verdant-userId');
9
+ if (!userId) {
10
+ userId = `user-${Math.random().toString(36).slice(2)}`;
11
+ localStorage.setItem('verdant-userId', userId);
12
+ }
13
+ return {
14
+ defaultProfile: { id: userId },
15
+ initialPresence,
16
+ authEndpoint: `http://localhost:${port}/auth/${libraryId}?userId=${userId}`,
17
+ };
18
+ }
@@ -0,0 +1,27 @@
1
+ import type { ClientDescriptor } from '../client/ClientDescriptor.js';
2
+
3
+ export async function registerBackgroundSync(clientDesc: ClientDescriptor) {
4
+ self.addEventListener('periodicsync', (event: any) => {
5
+ if (event.tag === 'verdant-sync') {
6
+ // See the "Think before you sync" section for
7
+ // checks you could perform before syncing.
8
+ event.waitUntil(sync(clientDesc));
9
+ }
10
+ });
11
+ }
12
+
13
+ async function sync(clientDesc: ClientDescriptor) {
14
+ try {
15
+ const client = await clientDesc.open();
16
+
17
+ await client.sync.syncOnce();
18
+ } catch (err) {
19
+ console.error('Failed to sync:', err);
20
+ if (err instanceof Error) {
21
+ localStorage.setItem(
22
+ 'backgroundSyncError',
23
+ `${err.name}: ${err.message}`,
24
+ );
25
+ }
26
+ }
27
+ }