dignity.js 0.4.0 → 0.5.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.
Files changed (38) hide show
  1. package/README.md +83 -2
  2. package/dist/dignity.cjs.js +542 -21
  3. package/dist/dignity.cjs.js.map +4 -4
  4. package/dist/dignity.esm.js +542 -21
  5. package/dist/dignity.esm.js.map +3 -3
  6. package/dist/dignity.min.js +18 -18
  7. package/docs/assets/dignity.esm.js +11205 -0
  8. package/docs/assets/favicon.svg +8 -0
  9. package/docs/chess/assets/chess-app.js +58022 -0
  10. package/docs/chess/assets/chess-app.js.map +7 -0
  11. package/docs/chess/assets/chess.css +584 -0
  12. package/docs/chess/favicon.ico +0 -0
  13. package/docs/chess/index.html +16 -0
  14. package/docs/chess/src/App.jsx +128 -0
  15. package/docs/chess/src/components/Board3D.jsx +364 -0
  16. package/docs/chess/src/components/GameView.jsx +847 -0
  17. package/docs/chess/src/components/JoinGate.jsx +68 -0
  18. package/docs/chess/src/components/LinkPanel.jsx +132 -0
  19. package/docs/chess/src/components/Lobby.jsx +154 -0
  20. package/docs/chess/src/components/MovePanel.jsx +123 -0
  21. package/docs/chess/src/lib/audio.js +50 -0
  22. package/docs/chess/src/lib/dignitySetup.js +42 -0
  23. package/docs/chess/src/lib/links.js +124 -0
  24. package/docs/chess/src/lib/localGames.js +160 -0
  25. package/docs/chess/src/lib/p2pDebug.js +192 -0
  26. package/docs/chess/src/main.jsx +5 -0
  27. package/docs/favicon.ico +0 -0
  28. package/docs/index.html +7 -3
  29. package/docs/openapi-like.json +35 -6
  30. package/examples/decentralized-chess-lite.js +52 -30
  31. package/package.json +13 -5
  32. package/src/core/dignity-p2p.js +388 -16
  33. package/src/index.js +6 -0
  34. package/src/network/peerjs-network.js +234 -0
  35. package/src/persistence/indexeddb-persistence.js +2 -0
  36. package/src/react/index.js +143 -1
  37. package/src/signaling/parse-peerjs-url.js +24 -0
  38. package/src/signaling/peerjs-signaling-provider.js +2 -8
@@ -11,9 +11,16 @@ const { MessageSecurityService } = require('../security/message-security-service
11
11
  * - update(collection, id, patch)
12
12
  * - remove(collection, id)
13
13
  *
14
+ * PeerJS / mesh replication helpers (see README "PeerJS mesh bootstrap"):
15
+ * - connectToPeer(peerId), getConnectionStats(), ensureConnectedToPeers(peerIds)
16
+ * - joinDiscovery(scope, { bootstrapPeerIds })
17
+ * - broadcastMessage(type, payload, { connectToPeers, broadcastScope })
18
+ * - pushRecordSnapshot(collection, id, options) — full record sync for late joiners
19
+ * - getRecordPeerIds(collection, id) — owner + collaborators for connectToPeers
20
+ *
14
21
  * Authorization model:
15
22
  * - object creator is the owner
16
- * - only owner can update or delete
23
+ * - only owner can update or delete (collaborators may update when listed)
17
24
  */
18
25
  class DignityP2P extends EventEmitter {
19
26
  constructor({ nodeId, networkAdapter, idGenerator, now, security } = {}) {
@@ -97,6 +104,7 @@ class DignityP2P extends EventEmitter {
97
104
  return {
98
105
  id: record.id,
99
106
  ownerId: record.ownerId,
107
+ collaboratorIds: Array.isArray(record.collaboratorIds) ? [...record.collaboratorIds] : [],
100
108
  createdAt: record.createdAt,
101
109
  updatedAt: record.updatedAt,
102
110
  version: record.version,
@@ -104,6 +112,79 @@ class DignityP2P extends EventEmitter {
104
112
  };
105
113
  }
106
114
 
115
+ canUpdateRecord(record, actorId) {
116
+ if (!record || !actorId) {
117
+ return false;
118
+ }
119
+
120
+ if (record.ownerId === actorId) {
121
+ return true;
122
+ }
123
+
124
+ return Array.isArray(record.collaboratorIds) && record.collaboratorIds.includes(actorId);
125
+ }
126
+
127
+ normalizeCollaboratorIds(collaborators) {
128
+ if (!Array.isArray(collaborators)) {
129
+ return [];
130
+ }
131
+
132
+ return [...new Set(collaborators.filter(Boolean))];
133
+ }
134
+
135
+ getRecordPeerIds(collectionName, id, options = {}) {
136
+ const record = options.fromRecord || this.getCollection(collectionName).get(id);
137
+ if (!record) {
138
+ return [];
139
+ }
140
+
141
+ const includeSelf = options.includeSelf === true;
142
+ const peerIds = [record.ownerId, ...(record.collaboratorIds || [])];
143
+
144
+ return [...new Set(peerIds.filter(Boolean).filter((peerId) => includeSelf || peerId !== this.nodeId))];
145
+ }
146
+
147
+ resolveReplicationPeers(collectionName, id, options = {}, hints = {}) {
148
+ if (options.connectToPeers === false) {
149
+ return undefined;
150
+ }
151
+
152
+ if (Array.isArray(options.connectToPeers)) {
153
+ return options.connectToPeers;
154
+ }
155
+
156
+ const peerIds = new Set();
157
+
158
+ if (hints.fromRecord) {
159
+ for (const peerId of this.getRecordPeerIds(collectionName, id, {
160
+ fromRecord: hints.fromRecord,
161
+ includeSelf: true
162
+ })) {
163
+ peerIds.add(peerId);
164
+ }
165
+ } else if (id) {
166
+ for (const peerId of this.getRecordPeerIds(collectionName, id, { includeSelf: true })) {
167
+ peerIds.add(peerId);
168
+ }
169
+ }
170
+
171
+ if (Array.isArray(options.collaborators)) {
172
+ for (const peerId of this.normalizeCollaboratorIds(options.collaborators)) {
173
+ peerIds.add(peerId);
174
+ }
175
+ }
176
+
177
+ if (Array.isArray(hints.extraPeerIds)) {
178
+ for (const peerId of hints.extraPeerIds) {
179
+ if (peerId) {
180
+ peerIds.add(peerId);
181
+ }
182
+ }
183
+ }
184
+
185
+ return [...peerIds].filter((peerId) => peerId && peerId !== this.nodeId);
186
+ }
187
+
107
188
  async create(collectionName, data, options = {}) {
108
189
  const collection = this.getCollection(collectionName);
109
190
  const id = options.id || this.idGenerator();
@@ -113,6 +194,7 @@ class DignityP2P extends EventEmitter {
113
194
  }
114
195
 
115
196
  const timestamp = this.now();
197
+ const collaboratorIds = this.normalizeCollaboratorIds(options.collaborators);
116
198
  const operation = {
117
199
  opId: this.idGenerator(),
118
200
  kind: 'create',
@@ -120,6 +202,7 @@ class DignityP2P extends EventEmitter {
120
202
  id,
121
203
  actorId: this.nodeId,
122
204
  ownerId: this.nodeId,
205
+ collaboratorIds,
123
206
  timestamp,
124
207
  payload: { ...data }
125
208
  };
@@ -130,6 +213,9 @@ class DignityP2P extends EventEmitter {
130
213
  messageType: 'operation',
131
214
  operation,
132
215
  collectionName
216
+ }),
217
+ connectToPeers: this.resolveReplicationPeers(collectionName, null, options, {
218
+ extraPeerIds: options.collaborators
133
219
  })
134
220
  });
135
221
 
@@ -174,8 +260,12 @@ class DignityP2P extends EventEmitter {
174
260
  throw new Error(`Object ${id} does not exist in ${collectionName}`);
175
261
  }
176
262
 
177
- if (existing.ownerId !== this.nodeId) {
178
- throw new Error(`Only owner ${existing.ownerId} can update object ${id}`);
263
+ if (!this.canUpdateRecord(existing, this.nodeId)) {
264
+ throw new Error(`Only owner ${existing.ownerId} or collaborators can update object ${id}`);
265
+ }
266
+
267
+ if (options.collaborators !== undefined && existing.ownerId !== this.nodeId) {
268
+ throw new Error(`Only owner ${existing.ownerId} can change collaborators on object ${id}`);
179
269
  }
180
270
 
181
271
  if (typeof options.expectedVersion === 'number' && existing.version !== options.expectedVersion) {
@@ -206,13 +296,18 @@ class DignityP2P extends EventEmitter {
206
296
  payload: { ...partialData }
207
297
  };
208
298
 
299
+ if (options.collaborators !== undefined) {
300
+ operation.collaboratorIds = this.normalizeCollaboratorIds(options.collaborators);
301
+ }
302
+
209
303
  this.applyOperation(operation);
210
304
  await this.broadcastMessage('operation', operation, {
211
305
  broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
212
306
  messageType: 'operation',
213
307
  operation,
214
308
  collectionName
215
- })
309
+ }),
310
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, { fromRecord: existing })
216
311
  });
217
312
 
218
313
  return this.read(collectionName, id);
@@ -243,6 +338,49 @@ class DignityP2P extends EventEmitter {
243
338
  throw new Error(`Unable to update ${collectionName}/${id} after ${maxAttempts} attempts`);
244
339
  }
245
340
 
341
+ async transferOwnership(collectionName, id, newOwnerId, options = {}) {
342
+ if (!newOwnerId) {
343
+ throw new Error('newOwnerId is required');
344
+ }
345
+
346
+ const existing = this.getCollection(collectionName).get(id);
347
+
348
+ if (!existing || existing.deletedAt) {
349
+ throw new Error(`Object ${id} does not exist in ${collectionName}`);
350
+ }
351
+
352
+ if (existing.ownerId !== this.nodeId) {
353
+ throw new Error(`Only owner ${existing.ownerId} can transfer object ${id}`);
354
+ }
355
+
356
+ const operation = {
357
+ opId: this.idGenerator(),
358
+ kind: 'transfer-ownership',
359
+ collectionName,
360
+ id,
361
+ actorId: this.nodeId,
362
+ timestamp: this.now(),
363
+ baseVersion: existing.version,
364
+ newOwnerId,
365
+ keepPreviousOwnerAsCollaborator: options.keepAsCollaborator !== false
366
+ };
367
+
368
+ this.applyOperation(operation);
369
+ await this.broadcastMessage('operation', operation, {
370
+ broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
371
+ messageType: 'operation',
372
+ operation,
373
+ collectionName
374
+ }),
375
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, {
376
+ fromRecord: existing,
377
+ extraPeerIds: [newOwnerId]
378
+ })
379
+ });
380
+
381
+ return this.read(collectionName, id);
382
+ }
383
+
246
384
  async remove(collectionName, id, options = {}) {
247
385
  const existing = this.getCollection(collectionName).get(id);
248
386
 
@@ -270,7 +408,8 @@ class DignityP2P extends EventEmitter {
270
408
  messageType: 'operation',
271
409
  operation,
272
410
  collectionName
273
- })
411
+ }),
412
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, { fromRecord: existing })
274
413
  });
275
414
  }
276
415
 
@@ -278,11 +417,88 @@ class DignityP2P extends EventEmitter {
278
417
  this.securityService.registerPeerPublicKey(peerId, publicKey);
279
418
  }
280
419
 
420
+ trustPeerPublicKey(peerId, publicKey) {
421
+ if (!peerId || !publicKey) {
422
+ return false;
423
+ }
424
+
425
+ try {
426
+ this.registerPeerPublicKey(peerId, publicKey);
427
+ return true;
428
+ } catch (error) {
429
+ this.emit('warning', { type: 'peer-key-trust-failed', peerId, error });
430
+ return false;
431
+ }
432
+ }
433
+
434
+ trustPeerFromMetadata(peerId, metadata) {
435
+ if (!metadata || !metadata.publicKey) {
436
+ return false;
437
+ }
438
+
439
+ return this.trustPeerPublicKey(peerId, metadata.publicKey);
440
+ }
441
+
281
442
  getPublicKey() {
282
443
  return this.securityService.getPublicKey();
283
444
  }
284
445
 
446
+ async connectToPeer(peerId) {
447
+ if (!peerId || peerId === this.nodeId) {
448
+ return null;
449
+ }
450
+
451
+ if (typeof this.networkAdapter.connectToPeer !== 'function') {
452
+ throw new Error('Network adapter does not support connectToPeer');
453
+ }
454
+
455
+ return this.networkAdapter.connectToPeer(peerId);
456
+ }
457
+
458
+ getConnectionStats() {
459
+ const adapter = this.networkAdapter;
460
+ if (!adapter) {
461
+ return { openCount: 0, peerIds: [] };
462
+ }
463
+
464
+ const peerIds = typeof adapter.listOpenPeerIds === 'function'
465
+ ? adapter.listOpenPeerIds()
466
+ : [];
467
+
468
+ const openCount = typeof adapter.getOpenConnectionCount === 'function'
469
+ ? adapter.getOpenConnectionCount()
470
+ : peerIds.length;
471
+
472
+ return { openCount, peerIds };
473
+ }
474
+
475
+ async ensureConnectedToPeers(peerIds = []) {
476
+ const normalized = [...new Set((peerIds || []).filter(Boolean))];
477
+ const results = [];
478
+
479
+ for (const peerId of normalized) {
480
+ if (peerId === this.nodeId) {
481
+ continue;
482
+ }
483
+
484
+ try {
485
+ await this.connectToPeer(peerId);
486
+ results.push({ peerId, ok: true });
487
+ } catch (error) {
488
+ this.emit('warning', { type: 'peer-connect-failed', peerId, error });
489
+ results.push({ peerId, ok: false, error });
490
+ }
491
+ }
492
+
493
+ return results;
494
+ }
495
+
285
496
  async broadcastMessage(messageType, payload, securityContext = {}) {
497
+ const connectToPeers = securityContext.connectToPeers;
498
+ if (Array.isArray(connectToPeers) && connectToPeers.length > 0) {
499
+ await this.ensureConnectedToPeers(connectToPeers);
500
+ }
501
+
286
502
  const envelope = await this.securityService.secureOutgoingMessage({
287
503
  messageType,
288
504
  payload,
@@ -293,6 +509,14 @@ class DignityP2P extends EventEmitter {
293
509
  }
294
510
 
295
511
  async sendDirectMessage(targetId, messageType, payload) {
512
+ if (targetId) {
513
+ try {
514
+ await this.connectToPeer(targetId);
515
+ } catch (error) {
516
+ this.emit('warning', { type: 'direct-message-connect-failed', targetId, error });
517
+ }
518
+ }
519
+
296
520
  const envelope = await this.securityService.secureOutgoingMessage({
297
521
  messageType,
298
522
  payload,
@@ -321,6 +545,8 @@ class DignityP2P extends EventEmitter {
321
545
  };
322
546
  map.set(peerId, next);
323
547
 
548
+ this.trustPeerFromMetadata(peerId, next.metadata);
549
+
324
550
  if (!existing) {
325
551
  this.emit('peerdiscovered', { scope, peerId, metadata: next.metadata });
326
552
  }
@@ -347,13 +573,23 @@ class DignityP2P extends EventEmitter {
347
573
  const normalizedScope = scope || 'main';
348
574
  const heartbeatIntervalMs = options.heartbeatIntervalMs || this.defaultDiscoveryHeartbeatMs;
349
575
  const ttlMs = options.ttlMs || this.defaultPresenceTtlMs;
350
- const metadata = options.metadata || {};
576
+ const metadata = {
577
+ publicKey: this.getPublicKey(),
578
+ ...(options.metadata || {})
579
+ };
580
+ const bootstrapPeerIds = Array.isArray(options.bootstrapPeerIds)
581
+ ? [...new Set(options.bootstrapPeerIds.filter(Boolean))]
582
+ : [];
351
583
 
352
584
  const existing = this.discoveryRooms.get(normalizedScope);
353
585
  if (existing && existing.timer) {
354
586
  clearInterval(existing.timer);
355
587
  }
356
588
 
589
+ if (bootstrapPeerIds.length > 0) {
590
+ await this.ensureConnectedToPeers(bootstrapPeerIds);
591
+ }
592
+
357
593
  const timer = setInterval(() => {
358
594
  this.announcePresence(normalizedScope).catch((error) => {
359
595
  this.emit('warning', { type: 'presence-heartbeat-failed', scope: normalizedScope, error });
@@ -362,6 +598,7 @@ class DignityP2P extends EventEmitter {
362
598
 
363
599
  this.discoveryRooms.set(normalizedScope, {
364
600
  metadata,
601
+ bootstrapPeerIds,
365
602
  heartbeatIntervalMs,
366
603
  ttlMs,
367
604
  timer
@@ -459,6 +696,10 @@ class DignityP2P extends EventEmitter {
459
696
  return;
460
697
  }
461
698
 
699
+ if (message && message.senderId && message.senderPublicKey) {
700
+ this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
701
+ }
702
+
462
703
  let decrypted;
463
704
  try {
464
705
  decrypted = await this.securityService.decryptIncomingMessage(message);
@@ -484,6 +725,23 @@ class DignityP2P extends EventEmitter {
484
725
  return;
485
726
  }
486
727
 
728
+ if (decrypted.messageType === 'record:snapshot') {
729
+ const payload = decrypted.payload || {};
730
+ const { collectionName, record } = payload;
731
+
732
+ if (collectionName && record) {
733
+ const applied = this.restoreRecord(collectionName, record);
734
+ if (applied) {
735
+ this.emit('change', {
736
+ kind: 'snapshot',
737
+ collection: collectionName,
738
+ id: record.id
739
+ });
740
+ }
741
+ }
742
+ return;
743
+ }
744
+
487
745
  if (decrypted.messageType === 'presence:announce') {
488
746
  const payload = decrypted.payload || {};
489
747
  const scope = payload.scope || 'main';
@@ -506,6 +764,12 @@ class DignityP2P extends EventEmitter {
506
764
  // Discovery handshake: when a new peer appears in a joined scope,
507
765
  // send our current presence so late joiners quickly converge.
508
766
  if (isNewPeerInScope && peerId !== this.nodeId && this.discoveryRooms.has(scope)) {
767
+ if (typeof this.networkAdapter.connectToPeer === 'function') {
768
+ Promise.resolve(this.connectToPeer(peerId)).catch((error) => {
769
+ this.emit('warning', { type: 'peer-connect-failed', scope, peerId, error });
770
+ });
771
+ }
772
+
509
773
  this.announcePresence(scope).catch((error) => {
510
774
  this.emit('warning', { type: 'presence-handshake-failed', scope, error });
511
775
  });
@@ -594,6 +858,7 @@ class DignityP2P extends EventEmitter {
594
858
  collection.set(record.id, {
595
859
  id: record.id,
596
860
  ownerId: record.ownerId,
861
+ collaboratorIds: this.normalizeCollaboratorIds(record.collaboratorIds),
597
862
  data: { ...(record.data || {}) },
598
863
  createdAt: record.createdAt,
599
864
  updatedAt: record.updatedAt,
@@ -604,6 +869,37 @@ class DignityP2P extends EventEmitter {
604
869
  return true;
605
870
  }
606
871
 
872
+ async pushRecordSnapshot(collectionName, id, options = {}) {
873
+ const collection = this.getCollection(collectionName);
874
+ const raw = collection.get(id);
875
+
876
+ if (!raw || raw.deletedAt) {
877
+ throw new Error(`Object ${id} does not exist in ${collectionName}`);
878
+ }
879
+
880
+ const record = {
881
+ id: raw.id,
882
+ ownerId: raw.ownerId,
883
+ collaboratorIds: Array.isArray(raw.collaboratorIds) ? [...raw.collaboratorIds] : [],
884
+ data: { ...raw.data },
885
+ createdAt: raw.createdAt,
886
+ updatedAt: raw.updatedAt,
887
+ deletedAt: raw.deletedAt || null,
888
+ version: raw.version
889
+ };
890
+
891
+ await this.broadcastMessage('record:snapshot', { collectionName, record }, {
892
+ broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
893
+ messageType: 'record:snapshot',
894
+ collectionName,
895
+ id
896
+ }),
897
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, { fromRecord: raw })
898
+ });
899
+
900
+ return record;
901
+ }
902
+
607
903
  applyOperation(operation) {
608
904
  if (!operation || !operation.opId || this.appliedOperations.has(operation.opId)) {
609
905
  return false;
@@ -620,6 +916,7 @@ class DignityP2P extends EventEmitter {
620
916
  collection.set(operation.id, {
621
917
  id: operation.id,
622
918
  ownerId: operation.ownerId,
919
+ collaboratorIds: this.normalizeCollaboratorIds(operation.collaboratorIds),
623
920
  data: { ...operation.payload },
624
921
  createdAt: operation.timestamp,
625
922
  updatedAt: operation.timestamp,
@@ -633,10 +930,90 @@ class DignityP2P extends EventEmitter {
633
930
  }
634
931
 
635
932
  if (!current || current.deletedAt) {
933
+ if (operation.kind !== 'create') {
934
+ this.emit('warning', {
935
+ type: 'orphan-operation',
936
+ kind: operation.kind,
937
+ collection: operation.collectionName,
938
+ id: operation.id,
939
+ actorId: operation.actorId,
940
+ hint: 'Peer is missing the record; pushRecordSnapshot from the owner to catch up.'
941
+ });
942
+ }
636
943
  return false;
637
944
  }
638
945
 
639
- if (operation.actorId !== current.ownerId) {
946
+ if (operation.kind === 'transfer-ownership') {
947
+ if (operation.actorId !== current.ownerId) {
948
+ return false;
949
+ }
950
+
951
+ if (typeof operation.baseVersion === 'number' && operation.baseVersion !== current.version) {
952
+ this.emitConflict({
953
+ kind: operation.kind,
954
+ collection: operation.collectionName,
955
+ id: operation.id,
956
+ expectedVersion: operation.baseVersion,
957
+ currentVersion: current.version,
958
+ phase: 'remote',
959
+ operation
960
+ });
961
+ return false;
962
+ }
963
+
964
+ const previousOwnerId = current.ownerId;
965
+ current.ownerId = operation.newOwnerId;
966
+
967
+ if (operation.keepPreviousOwnerAsCollaborator !== false) {
968
+ const collaborators = this.normalizeCollaboratorIds(current.collaboratorIds);
969
+ if (!collaborators.includes(previousOwnerId)) {
970
+ collaborators.push(previousOwnerId);
971
+ }
972
+ current.collaboratorIds = collaborators.filter((peerId) => peerId !== operation.newOwnerId);
973
+ }
974
+
975
+ current.updatedAt = operation.timestamp;
976
+ current.version += 1;
977
+
978
+ this.appliedOperations.add(operation.opId);
979
+ this.emit('change', {
980
+ kind: 'transfer-ownership',
981
+ collection: operation.collectionName,
982
+ id: operation.id,
983
+ previousOwnerId,
984
+ newOwnerId: operation.newOwnerId
985
+ });
986
+ return true;
987
+ }
988
+
989
+ if (operation.kind === 'delete') {
990
+ if (operation.actorId !== current.ownerId) {
991
+ return false;
992
+ }
993
+
994
+ if (typeof operation.baseVersion === 'number' && operation.baseVersion !== current.version) {
995
+ this.emitConflict({
996
+ kind: operation.kind,
997
+ collection: operation.collectionName,
998
+ id: operation.id,
999
+ expectedVersion: operation.baseVersion,
1000
+ currentVersion: current.version,
1001
+ phase: 'remote',
1002
+ operation
1003
+ });
1004
+ return false;
1005
+ }
1006
+
1007
+ current.deletedAt = operation.timestamp;
1008
+ current.updatedAt = operation.timestamp;
1009
+ current.version += 1;
1010
+
1011
+ this.appliedOperations.add(operation.opId);
1012
+ this.emit('change', { kind: 'delete', collection: operation.collectionName, id: operation.id });
1013
+ return true;
1014
+ }
1015
+
1016
+ if (!this.canUpdateRecord(current, operation.actorId)) {
640
1017
  return false;
641
1018
  }
642
1019
 
@@ -658,21 +1035,16 @@ class DignityP2P extends EventEmitter {
658
1035
  ...current.data,
659
1036
  ...operation.payload
660
1037
  };
661
- current.updatedAt = operation.timestamp;
662
- current.version += 1;
663
1038
 
664
- this.appliedOperations.add(operation.opId);
665
- this.emit('change', { kind: 'update', collection: operation.collectionName, id: operation.id });
666
- return true;
667
- }
1039
+ if (Array.isArray(operation.collaboratorIds) && operation.actorId === current.ownerId) {
1040
+ current.collaboratorIds = this.normalizeCollaboratorIds(operation.collaboratorIds);
1041
+ }
668
1042
 
669
- if (operation.kind === 'delete') {
670
- current.deletedAt = operation.timestamp;
671
1043
  current.updatedAt = operation.timestamp;
672
1044
  current.version += 1;
673
1045
 
674
1046
  this.appliedOperations.add(operation.opId);
675
- this.emit('change', { kind: 'delete', collection: operation.collectionName, id: operation.id });
1047
+ this.emit('change', { kind: 'update', collection: operation.collectionName, id: operation.id });
676
1048
  return true;
677
1049
  }
678
1050
 
package/src/index.js CHANGED
@@ -15,6 +15,10 @@ const {
15
15
  InMemoryNetworkHub,
16
16
  InMemoryNetworkAdapter
17
17
  } = require('./network/in-memory-network');
18
+ const {
19
+ PeerJSNetworkAdapter,
20
+ createPeerJSNetworkAdapter
21
+ } = require('./network/peerjs-network');
18
22
  const IndexedDBPersistence = require('./persistence/indexeddb-persistence');
19
23
  const {
20
24
  DEFAULT_CLOUDFLARE_SIGNALING_URLS,
@@ -35,6 +39,8 @@ module.exports = {
35
39
  PeerJSSignalingProvider,
36
40
  InMemoryNetworkHub,
37
41
  InMemoryNetworkAdapter,
42
+ PeerJSNetworkAdapter,
43
+ createPeerJSNetworkAdapter,
38
44
  IndexedDBPersistence,
39
45
  DEFAULT_CLOUDFLARE_SIGNALING_URLS,
40
46
  DEFAULT_SIGNALING_FALLBACK_URLS,