dignity.js 0.4.0 → 0.5.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.
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 +12 -4
  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
@@ -2914,12 +2914,71 @@ var require_dignity_p2p = __commonJS({
2914
2914
  return {
2915
2915
  id: record.id,
2916
2916
  ownerId: record.ownerId,
2917
+ collaboratorIds: Array.isArray(record.collaboratorIds) ? [...record.collaboratorIds] : [],
2917
2918
  createdAt: record.createdAt,
2918
2919
  updatedAt: record.updatedAt,
2919
2920
  version: record.version,
2920
2921
  data: { ...record.data }
2921
2922
  };
2922
2923
  }
2924
+ canUpdateRecord(record, actorId) {
2925
+ if (!record || !actorId) {
2926
+ return false;
2927
+ }
2928
+ if (record.ownerId === actorId) {
2929
+ return true;
2930
+ }
2931
+ return Array.isArray(record.collaboratorIds) && record.collaboratorIds.includes(actorId);
2932
+ }
2933
+ normalizeCollaboratorIds(collaborators) {
2934
+ if (!Array.isArray(collaborators)) {
2935
+ return [];
2936
+ }
2937
+ return [...new Set(collaborators.filter(Boolean))];
2938
+ }
2939
+ getRecordPeerIds(collectionName, id, options = {}) {
2940
+ const record = options.fromRecord || this.getCollection(collectionName).get(id);
2941
+ if (!record) {
2942
+ return [];
2943
+ }
2944
+ const includeSelf = options.includeSelf === true;
2945
+ const peerIds = [record.ownerId, ...record.collaboratorIds || []];
2946
+ return [...new Set(peerIds.filter(Boolean).filter((peerId) => includeSelf || peerId !== this.nodeId))];
2947
+ }
2948
+ resolveReplicationPeers(collectionName, id, options = {}, hints = {}) {
2949
+ if (options.connectToPeers === false) {
2950
+ return void 0;
2951
+ }
2952
+ if (Array.isArray(options.connectToPeers)) {
2953
+ return options.connectToPeers;
2954
+ }
2955
+ const peerIds = /* @__PURE__ */ new Set();
2956
+ if (hints.fromRecord) {
2957
+ for (const peerId of this.getRecordPeerIds(collectionName, id, {
2958
+ fromRecord: hints.fromRecord,
2959
+ includeSelf: true
2960
+ })) {
2961
+ peerIds.add(peerId);
2962
+ }
2963
+ } else if (id) {
2964
+ for (const peerId of this.getRecordPeerIds(collectionName, id, { includeSelf: true })) {
2965
+ peerIds.add(peerId);
2966
+ }
2967
+ }
2968
+ if (Array.isArray(options.collaborators)) {
2969
+ for (const peerId of this.normalizeCollaboratorIds(options.collaborators)) {
2970
+ peerIds.add(peerId);
2971
+ }
2972
+ }
2973
+ if (Array.isArray(hints.extraPeerIds)) {
2974
+ for (const peerId of hints.extraPeerIds) {
2975
+ if (peerId) {
2976
+ peerIds.add(peerId);
2977
+ }
2978
+ }
2979
+ }
2980
+ return [...peerIds].filter((peerId) => peerId && peerId !== this.nodeId);
2981
+ }
2923
2982
  async create(collectionName, data, options = {}) {
2924
2983
  const collection = this.getCollection(collectionName);
2925
2984
  const id = options.id || this.idGenerator();
@@ -2927,6 +2986,7 @@ var require_dignity_p2p = __commonJS({
2927
2986
  throw new Error(`Object ${id} already exists in ${collectionName}`);
2928
2987
  }
2929
2988
  const timestamp = this.now();
2989
+ const collaboratorIds = this.normalizeCollaboratorIds(options.collaborators);
2930
2990
  const operation = {
2931
2991
  opId: this.idGenerator(),
2932
2992
  kind: "create",
@@ -2934,6 +2994,7 @@ var require_dignity_p2p = __commonJS({
2934
2994
  id,
2935
2995
  actorId: this.nodeId,
2936
2996
  ownerId: this.nodeId,
2997
+ collaboratorIds,
2937
2998
  timestamp,
2938
2999
  payload: { ...data }
2939
3000
  };
@@ -2943,6 +3004,9 @@ var require_dignity_p2p = __commonJS({
2943
3004
  messageType: "operation",
2944
3005
  operation,
2945
3006
  collectionName
3007
+ }),
3008
+ connectToPeers: this.resolveReplicationPeers(collectionName, null, options, {
3009
+ extraPeerIds: options.collaborators
2946
3010
  })
2947
3011
  });
2948
3012
  return this.read(collectionName, id);
@@ -2977,8 +3041,11 @@ var require_dignity_p2p = __commonJS({
2977
3041
  if (!existing || existing.deletedAt) {
2978
3042
  throw new Error(`Object ${id} does not exist in ${collectionName}`);
2979
3043
  }
2980
- if (existing.ownerId !== this.nodeId) {
2981
- throw new Error(`Only owner ${existing.ownerId} can update object ${id}`);
3044
+ if (!this.canUpdateRecord(existing, this.nodeId)) {
3045
+ throw new Error(`Only owner ${existing.ownerId} or collaborators can update object ${id}`);
3046
+ }
3047
+ if (options.collaborators !== void 0 && existing.ownerId !== this.nodeId) {
3048
+ throw new Error(`Only owner ${existing.ownerId} can change collaborators on object ${id}`);
2982
3049
  }
2983
3050
  if (typeof options.expectedVersion === "number" && existing.version !== options.expectedVersion) {
2984
3051
  this.emitConflict({
@@ -3005,13 +3072,17 @@ var require_dignity_p2p = __commonJS({
3005
3072
  baseVersion: existing.version,
3006
3073
  payload: { ...partialData }
3007
3074
  };
3075
+ if (options.collaborators !== void 0) {
3076
+ operation.collaboratorIds = this.normalizeCollaboratorIds(options.collaborators);
3077
+ }
3008
3078
  this.applyOperation(operation);
3009
3079
  await this.broadcastMessage("operation", operation, {
3010
3080
  broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
3011
3081
  messageType: "operation",
3012
3082
  operation,
3013
3083
  collectionName
3014
- })
3084
+ }),
3085
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, { fromRecord: existing })
3015
3086
  });
3016
3087
  return this.read(collectionName, id);
3017
3088
  }
@@ -3036,6 +3107,42 @@ var require_dignity_p2p = __commonJS({
3036
3107
  }
3037
3108
  throw new Error(`Unable to update ${collectionName}/${id} after ${maxAttempts} attempts`);
3038
3109
  }
3110
+ async transferOwnership(collectionName, id, newOwnerId, options = {}) {
3111
+ if (!newOwnerId) {
3112
+ throw new Error("newOwnerId is required");
3113
+ }
3114
+ const existing = this.getCollection(collectionName).get(id);
3115
+ if (!existing || existing.deletedAt) {
3116
+ throw new Error(`Object ${id} does not exist in ${collectionName}`);
3117
+ }
3118
+ if (existing.ownerId !== this.nodeId) {
3119
+ throw new Error(`Only owner ${existing.ownerId} can transfer object ${id}`);
3120
+ }
3121
+ const operation = {
3122
+ opId: this.idGenerator(),
3123
+ kind: "transfer-ownership",
3124
+ collectionName,
3125
+ id,
3126
+ actorId: this.nodeId,
3127
+ timestamp: this.now(),
3128
+ baseVersion: existing.version,
3129
+ newOwnerId,
3130
+ keepPreviousOwnerAsCollaborator: options.keepAsCollaborator !== false
3131
+ };
3132
+ this.applyOperation(operation);
3133
+ await this.broadcastMessage("operation", operation, {
3134
+ broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
3135
+ messageType: "operation",
3136
+ operation,
3137
+ collectionName
3138
+ }),
3139
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, {
3140
+ fromRecord: existing,
3141
+ extraPeerIds: [newOwnerId]
3142
+ })
3143
+ });
3144
+ return this.read(collectionName, id);
3145
+ }
3039
3146
  async remove(collectionName, id, options = {}) {
3040
3147
  const existing = this.getCollection(collectionName).get(id);
3041
3148
  if (!existing || existing.deletedAt) {
@@ -3059,16 +3166,74 @@ var require_dignity_p2p = __commonJS({
3059
3166
  messageType: "operation",
3060
3167
  operation,
3061
3168
  collectionName
3062
- })
3169
+ }),
3170
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, { fromRecord: existing })
3063
3171
  });
3064
3172
  }
3065
3173
  registerPeerPublicKey(peerId, publicKey) {
3066
3174
  this.securityService.registerPeerPublicKey(peerId, publicKey);
3067
3175
  }
3176
+ trustPeerPublicKey(peerId, publicKey) {
3177
+ if (!peerId || !publicKey) {
3178
+ return false;
3179
+ }
3180
+ try {
3181
+ this.registerPeerPublicKey(peerId, publicKey);
3182
+ return true;
3183
+ } catch (error) {
3184
+ this.emit("warning", { type: "peer-key-trust-failed", peerId, error });
3185
+ return false;
3186
+ }
3187
+ }
3188
+ trustPeerFromMetadata(peerId, metadata) {
3189
+ if (!metadata || !metadata.publicKey) {
3190
+ return false;
3191
+ }
3192
+ return this.trustPeerPublicKey(peerId, metadata.publicKey);
3193
+ }
3068
3194
  getPublicKey() {
3069
3195
  return this.securityService.getPublicKey();
3070
3196
  }
3197
+ async connectToPeer(peerId) {
3198
+ if (!peerId || peerId === this.nodeId) {
3199
+ return null;
3200
+ }
3201
+ if (typeof this.networkAdapter.connectToPeer !== "function") {
3202
+ throw new Error("Network adapter does not support connectToPeer");
3203
+ }
3204
+ return this.networkAdapter.connectToPeer(peerId);
3205
+ }
3206
+ getConnectionStats() {
3207
+ const adapter = this.networkAdapter;
3208
+ if (!adapter) {
3209
+ return { openCount: 0, peerIds: [] };
3210
+ }
3211
+ const peerIds = typeof adapter.listOpenPeerIds === "function" ? adapter.listOpenPeerIds() : [];
3212
+ const openCount = typeof adapter.getOpenConnectionCount === "function" ? adapter.getOpenConnectionCount() : peerIds.length;
3213
+ return { openCount, peerIds };
3214
+ }
3215
+ async ensureConnectedToPeers(peerIds = []) {
3216
+ const normalized = [...new Set((peerIds || []).filter(Boolean))];
3217
+ const results = [];
3218
+ for (const peerId of normalized) {
3219
+ if (peerId === this.nodeId) {
3220
+ continue;
3221
+ }
3222
+ try {
3223
+ await this.connectToPeer(peerId);
3224
+ results.push({ peerId, ok: true });
3225
+ } catch (error) {
3226
+ this.emit("warning", { type: "peer-connect-failed", peerId, error });
3227
+ results.push({ peerId, ok: false, error });
3228
+ }
3229
+ }
3230
+ return results;
3231
+ }
3071
3232
  async broadcastMessage(messageType, payload, securityContext = {}) {
3233
+ const connectToPeers = securityContext.connectToPeers;
3234
+ if (Array.isArray(connectToPeers) && connectToPeers.length > 0) {
3235
+ await this.ensureConnectedToPeers(connectToPeers);
3236
+ }
3072
3237
  const envelope = await this.securityService.secureOutgoingMessage({
3073
3238
  messageType,
3074
3239
  payload,
@@ -3078,6 +3243,13 @@ var require_dignity_p2p = __commonJS({
3078
3243
  await this.networkAdapter.broadcast(envelope);
3079
3244
  }
3080
3245
  async sendDirectMessage(targetId, messageType, payload) {
3246
+ if (targetId) {
3247
+ try {
3248
+ await this.connectToPeer(targetId);
3249
+ } catch (error) {
3250
+ this.emit("warning", { type: "direct-message-connect-failed", targetId, error });
3251
+ }
3252
+ }
3081
3253
  const envelope = await this.securityService.secureOutgoingMessage({
3082
3254
  messageType,
3083
3255
  payload,
@@ -3102,6 +3274,7 @@ var require_dignity_p2p = __commonJS({
3102
3274
  expiresAt: announcedAt + ttlMs
3103
3275
  };
3104
3276
  map.set(peerId, next);
3277
+ this.trustPeerFromMetadata(peerId, next.metadata);
3105
3278
  if (!existing) {
3106
3279
  this.emit("peerdiscovered", { scope, peerId, metadata: next.metadata });
3107
3280
  }
@@ -3124,11 +3297,18 @@ var require_dignity_p2p = __commonJS({
3124
3297
  const normalizedScope = scope || "main";
3125
3298
  const heartbeatIntervalMs = options.heartbeatIntervalMs || this.defaultDiscoveryHeartbeatMs;
3126
3299
  const ttlMs = options.ttlMs || this.defaultPresenceTtlMs;
3127
- const metadata = options.metadata || {};
3300
+ const metadata = {
3301
+ publicKey: this.getPublicKey(),
3302
+ ...options.metadata || {}
3303
+ };
3304
+ const bootstrapPeerIds = Array.isArray(options.bootstrapPeerIds) ? [...new Set(options.bootstrapPeerIds.filter(Boolean))] : [];
3128
3305
  const existing = this.discoveryRooms.get(normalizedScope);
3129
3306
  if (existing && existing.timer) {
3130
3307
  clearInterval(existing.timer);
3131
3308
  }
3309
+ if (bootstrapPeerIds.length > 0) {
3310
+ await this.ensureConnectedToPeers(bootstrapPeerIds);
3311
+ }
3132
3312
  const timer = setInterval(() => {
3133
3313
  this.announcePresence(normalizedScope).catch((error) => {
3134
3314
  this.emit("warning", { type: "presence-heartbeat-failed", scope: normalizedScope, error });
@@ -3136,6 +3316,7 @@ var require_dignity_p2p = __commonJS({
3136
3316
  }, heartbeatIntervalMs);
3137
3317
  this.discoveryRooms.set(normalizedScope, {
3138
3318
  metadata,
3319
+ bootstrapPeerIds,
3139
3320
  heartbeatIntervalMs,
3140
3321
  ttlMs,
3141
3322
  timer
@@ -3216,6 +3397,9 @@ var require_dignity_p2p = __commonJS({
3216
3397
  });
3217
3398
  return;
3218
3399
  }
3400
+ if (message && message.senderId && message.senderPublicKey) {
3401
+ this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
3402
+ }
3219
3403
  let decrypted;
3220
3404
  try {
3221
3405
  decrypted = await this.securityService.decryptIncomingMessage(message);
@@ -3237,6 +3421,21 @@ var require_dignity_p2p = __commonJS({
3237
3421
  this.applyOperation(decrypted.payload);
3238
3422
  return;
3239
3423
  }
3424
+ if (decrypted.messageType === "record:snapshot") {
3425
+ const payload = decrypted.payload || {};
3426
+ const { collectionName, record } = payload;
3427
+ if (collectionName && record) {
3428
+ const applied = this.restoreRecord(collectionName, record);
3429
+ if (applied) {
3430
+ this.emit("change", {
3431
+ kind: "snapshot",
3432
+ collection: collectionName,
3433
+ id: record.id
3434
+ });
3435
+ }
3436
+ }
3437
+ return;
3438
+ }
3240
3439
  if (decrypted.messageType === "presence:announce") {
3241
3440
  const payload = decrypted.payload || {};
3242
3441
  const scope = payload.scope || "main";
@@ -3254,6 +3453,11 @@ var require_dignity_p2p = __commonJS({
3254
3453
  payload.announcedAt || this.now()
3255
3454
  );
3256
3455
  if (isNewPeerInScope && peerId !== this.nodeId && this.discoveryRooms.has(scope)) {
3456
+ if (typeof this.networkAdapter.connectToPeer === "function") {
3457
+ Promise.resolve(this.connectToPeer(peerId)).catch((error) => {
3458
+ this.emit("warning", { type: "peer-connect-failed", scope, peerId, error });
3459
+ });
3460
+ }
3257
3461
  this.announcePresence(scope).catch((error) => {
3258
3462
  this.emit("warning", { type: "presence-handshake-failed", scope, error });
3259
3463
  });
@@ -3328,6 +3532,7 @@ var require_dignity_p2p = __commonJS({
3328
3532
  collection.set(record.id, {
3329
3533
  id: record.id,
3330
3534
  ownerId: record.ownerId,
3535
+ collaboratorIds: this.normalizeCollaboratorIds(record.collaboratorIds),
3331
3536
  data: { ...record.data || {} },
3332
3537
  createdAt: record.createdAt,
3333
3538
  updatedAt: record.updatedAt,
@@ -3336,6 +3541,32 @@ var require_dignity_p2p = __commonJS({
3336
3541
  });
3337
3542
  return true;
3338
3543
  }
3544
+ async pushRecordSnapshot(collectionName, id, options = {}) {
3545
+ const collection = this.getCollection(collectionName);
3546
+ const raw = collection.get(id);
3547
+ if (!raw || raw.deletedAt) {
3548
+ throw new Error(`Object ${id} does not exist in ${collectionName}`);
3549
+ }
3550
+ const record = {
3551
+ id: raw.id,
3552
+ ownerId: raw.ownerId,
3553
+ collaboratorIds: Array.isArray(raw.collaboratorIds) ? [...raw.collaboratorIds] : [],
3554
+ data: { ...raw.data },
3555
+ createdAt: raw.createdAt,
3556
+ updatedAt: raw.updatedAt,
3557
+ deletedAt: raw.deletedAt || null,
3558
+ version: raw.version
3559
+ };
3560
+ await this.broadcastMessage("record:snapshot", { collectionName, record }, {
3561
+ broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
3562
+ messageType: "record:snapshot",
3563
+ collectionName,
3564
+ id
3565
+ }),
3566
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, { fromRecord: raw })
3567
+ });
3568
+ return record;
3569
+ }
3339
3570
  applyOperation(operation) {
3340
3571
  if (!operation || !operation.opId || this.appliedOperations.has(operation.opId)) {
3341
3572
  return false;
@@ -3349,6 +3580,7 @@ var require_dignity_p2p = __commonJS({
3349
3580
  collection.set(operation.id, {
3350
3581
  id: operation.id,
3351
3582
  ownerId: operation.ownerId,
3583
+ collaboratorIds: this.normalizeCollaboratorIds(operation.collaboratorIds),
3352
3584
  data: { ...operation.payload },
3353
3585
  createdAt: operation.timestamp,
3354
3586
  updatedAt: operation.timestamp,
@@ -3360,9 +3592,79 @@ var require_dignity_p2p = __commonJS({
3360
3592
  return true;
3361
3593
  }
3362
3594
  if (!current || current.deletedAt) {
3595
+ if (operation.kind !== "create") {
3596
+ this.emit("warning", {
3597
+ type: "orphan-operation",
3598
+ kind: operation.kind,
3599
+ collection: operation.collectionName,
3600
+ id: operation.id,
3601
+ actorId: operation.actorId,
3602
+ hint: "Peer is missing the record; pushRecordSnapshot from the owner to catch up."
3603
+ });
3604
+ }
3363
3605
  return false;
3364
3606
  }
3365
- if (operation.actorId !== current.ownerId) {
3607
+ if (operation.kind === "transfer-ownership") {
3608
+ if (operation.actorId !== current.ownerId) {
3609
+ return false;
3610
+ }
3611
+ if (typeof operation.baseVersion === "number" && operation.baseVersion !== current.version) {
3612
+ this.emitConflict({
3613
+ kind: operation.kind,
3614
+ collection: operation.collectionName,
3615
+ id: operation.id,
3616
+ expectedVersion: operation.baseVersion,
3617
+ currentVersion: current.version,
3618
+ phase: "remote",
3619
+ operation
3620
+ });
3621
+ return false;
3622
+ }
3623
+ const previousOwnerId = current.ownerId;
3624
+ current.ownerId = operation.newOwnerId;
3625
+ if (operation.keepPreviousOwnerAsCollaborator !== false) {
3626
+ const collaborators = this.normalizeCollaboratorIds(current.collaboratorIds);
3627
+ if (!collaborators.includes(previousOwnerId)) {
3628
+ collaborators.push(previousOwnerId);
3629
+ }
3630
+ current.collaboratorIds = collaborators.filter((peerId) => peerId !== operation.newOwnerId);
3631
+ }
3632
+ current.updatedAt = operation.timestamp;
3633
+ current.version += 1;
3634
+ this.appliedOperations.add(operation.opId);
3635
+ this.emit("change", {
3636
+ kind: "transfer-ownership",
3637
+ collection: operation.collectionName,
3638
+ id: operation.id,
3639
+ previousOwnerId,
3640
+ newOwnerId: operation.newOwnerId
3641
+ });
3642
+ return true;
3643
+ }
3644
+ if (operation.kind === "delete") {
3645
+ if (operation.actorId !== current.ownerId) {
3646
+ return false;
3647
+ }
3648
+ if (typeof operation.baseVersion === "number" && operation.baseVersion !== current.version) {
3649
+ this.emitConflict({
3650
+ kind: operation.kind,
3651
+ collection: operation.collectionName,
3652
+ id: operation.id,
3653
+ expectedVersion: operation.baseVersion,
3654
+ currentVersion: current.version,
3655
+ phase: "remote",
3656
+ operation
3657
+ });
3658
+ return false;
3659
+ }
3660
+ current.deletedAt = operation.timestamp;
3661
+ current.updatedAt = operation.timestamp;
3662
+ current.version += 1;
3663
+ this.appliedOperations.add(operation.opId);
3664
+ this.emit("change", { kind: "delete", collection: operation.collectionName, id: operation.id });
3665
+ return true;
3666
+ }
3667
+ if (!this.canUpdateRecord(current, operation.actorId)) {
3366
3668
  return false;
3367
3669
  }
3368
3670
  if (typeof operation.baseVersion === "number" && operation.baseVersion !== current.version) {
@@ -3382,20 +3684,15 @@ var require_dignity_p2p = __commonJS({
3382
3684
  ...current.data,
3383
3685
  ...operation.payload
3384
3686
  };
3687
+ if (Array.isArray(operation.collaboratorIds) && operation.actorId === current.ownerId) {
3688
+ current.collaboratorIds = this.normalizeCollaboratorIds(operation.collaboratorIds);
3689
+ }
3385
3690
  current.updatedAt = operation.timestamp;
3386
3691
  current.version += 1;
3387
3692
  this.appliedOperations.add(operation.opId);
3388
3693
  this.emit("change", { kind: "update", collection: operation.collectionName, id: operation.id });
3389
3694
  return true;
3390
3695
  }
3391
- if (operation.kind === "delete") {
3392
- current.deletedAt = operation.timestamp;
3393
- current.updatedAt = operation.timestamp;
3394
- current.version += 1;
3395
- this.appliedOperations.add(operation.opId);
3396
- this.emit("change", { kind: "delete", collection: operation.collectionName, id: operation.id });
3397
- return true;
3398
- }
3399
3696
  return false;
3400
3697
  }
3401
3698
  };
@@ -3550,6 +3847,28 @@ var require_websocket_signaling_provider = __commonJS({
3550
3847
  }
3551
3848
  });
3552
3849
 
3850
+ // src/signaling/parse-peerjs-url.js
3851
+ var require_parse_peerjs_url = __commonJS({
3852
+ "src/signaling/parse-peerjs-url.js"(exports2, module2) {
3853
+ function parsePeerJsServerUrl(url) {
3854
+ const parsed = new URL(url);
3855
+ const secure = parsed.protocol === "wss:";
3856
+ const host = parsed.hostname;
3857
+ const port = parsed.port ? Number(parsed.port) : secure ? 443 : 80;
3858
+ const key = parsed.searchParams.get("key") || "peerjs";
3859
+ let path = parsed.pathname || "/";
3860
+ if (path.endsWith("/peerjs")) {
3861
+ path = path.slice(0, -"/peerjs".length) || "/";
3862
+ }
3863
+ if (path !== "/" && !path.endsWith("/")) {
3864
+ path += "/";
3865
+ }
3866
+ return { secure, host, port, path, key };
3867
+ }
3868
+ module2.exports = parsePeerJsServerUrl;
3869
+ }
3870
+ });
3871
+
3553
3872
  // node_modules/peerjs-js-binarypack/dist/binarypack.cjs
3554
3873
  var require_binarypack = __commonJS({
3555
3874
  "node_modules/peerjs-js-binarypack/dist/binarypack.cjs"(exports2, module2) {
@@ -10139,6 +10458,7 @@ var require_bundler = __commonJS({
10139
10458
  var require_peerjs_signaling_provider = __commonJS({
10140
10459
  "src/signaling/peerjs-signaling-provider.js"(exports2, module2) {
10141
10460
  var WebSocketSignalingProvider2 = require_websocket_signaling_provider();
10461
+ var parsePeerJsServerUrl = require_parse_peerjs_url();
10142
10462
  var PeerJSSignalingProvider2 = class {
10143
10463
  constructor({ id, url, PeerImpl, WebSocketImpl, priority = 0, connectTimeoutMs = 1e4 }) {
10144
10464
  if (!url) {
@@ -10166,13 +10486,7 @@ var require_peerjs_signaling_provider = __commonJS({
10166
10486
  }
10167
10487
  }
10168
10488
  parsePeerJsServerUrl() {
10169
- const parsed = new URL(this.url);
10170
- const secure = parsed.protocol === "wss:";
10171
- const host = parsed.hostname;
10172
- const port = parsed.port ? Number(parsed.port) : secure ? 443 : 80;
10173
- const path = parsed.pathname || "/";
10174
- const key = parsed.searchParams.get("key") || "peerjs";
10175
- return { secure, host, port, path, key };
10489
+ return parsePeerJsServerUrl(this.url);
10176
10490
  }
10177
10491
  shouldUseWebSocketFallback() {
10178
10492
  return !this.isCustomPeerImpl && typeof globalThis.RTCPeerConnection !== "function";
@@ -10452,6 +10766,205 @@ var require_in_memory_network = __commonJS({
10452
10766
  }
10453
10767
  });
10454
10768
 
10769
+ // src/network/peerjs-network.js
10770
+ var require_peerjs_network = __commonJS({
10771
+ "src/network/peerjs-network.js"(exports2, module2) {
10772
+ var { DEFAULT_CLOUDFLARE_SIGNALING_URLS: DEFAULT_CLOUDFLARE_SIGNALING_URLS2 } = require_default_signaling_config();
10773
+ var parsePeerJsServerUrl = require_parse_peerjs_url();
10774
+ function resolvePeerImplementation(PeerImpl) {
10775
+ if (PeerImpl) {
10776
+ return PeerImpl;
10777
+ }
10778
+ try {
10779
+ const peerjs = require_bundler();
10780
+ return peerjs.Peer || peerjs;
10781
+ } catch (error) {
10782
+ return null;
10783
+ }
10784
+ }
10785
+ var PeerJSNetworkAdapter2 = class {
10786
+ constructor({
10787
+ url,
10788
+ urls,
10789
+ PeerImpl,
10790
+ connectTimeoutMs = 12e3
10791
+ } = {}) {
10792
+ this.urls = urls || (url ? [url] : [...DEFAULT_CLOUDFLARE_SIGNALING_URLS2]);
10793
+ this.url = this.urls[0];
10794
+ this.PeerImpl = resolvePeerImplementation(PeerImpl);
10795
+ this.connectTimeoutMs = connectTimeoutMs;
10796
+ this.nodeId = null;
10797
+ this.peer = null;
10798
+ this.connections = /* @__PURE__ */ new Map();
10799
+ this.pendingConnections = /* @__PURE__ */ new Map();
10800
+ this.messageHandlers = /* @__PURE__ */ new Set();
10801
+ }
10802
+ async start(nodeId) {
10803
+ if (!nodeId) {
10804
+ throw new Error("PeerJSNetworkAdapter requires nodeId on start");
10805
+ }
10806
+ if (!this.PeerImpl) {
10807
+ throw new Error("PeerJS implementation is not available");
10808
+ }
10809
+ if (this.peer) {
10810
+ await this.stop();
10811
+ }
10812
+ let lastError;
10813
+ for (const candidateUrl of this.urls) {
10814
+ try {
10815
+ await this.startWithUrl(nodeId, candidateUrl);
10816
+ this.url = candidateUrl;
10817
+ return;
10818
+ } catch (error) {
10819
+ lastError = error;
10820
+ }
10821
+ }
10822
+ throw lastError || new Error("Unable to connect PeerJS network adapter");
10823
+ }
10824
+ async startWithUrl(nodeId, url) {
10825
+ this.nodeId = nodeId;
10826
+ const server = parsePeerJsServerUrl(url);
10827
+ await new Promise((resolve, reject) => {
10828
+ const peer = new this.PeerImpl(nodeId, {
10829
+ host: server.host,
10830
+ port: server.port,
10831
+ path: server.path,
10832
+ secure: server.secure,
10833
+ key: server.key
10834
+ });
10835
+ const timeout = setTimeout(() => {
10836
+ peer.destroy?.();
10837
+ reject(new Error(`Unable to connect PeerJS network adapter to ${url}`));
10838
+ }, this.connectTimeoutMs);
10839
+ peer.on("open", () => {
10840
+ clearTimeout(timeout);
10841
+ this.peer = peer;
10842
+ resolve();
10843
+ });
10844
+ peer.on("connection", (connection) => {
10845
+ this.attachConnectionHandlers(connection);
10846
+ });
10847
+ peer.on("error", (error) => {
10848
+ clearTimeout(timeout);
10849
+ peer.destroy?.();
10850
+ reject(error || new Error(`Unable to connect PeerJS network adapter to ${url}`));
10851
+ });
10852
+ });
10853
+ }
10854
+ attachConnectionHandlers(connection) {
10855
+ const remoteId = connection.peer;
10856
+ if (!remoteId) {
10857
+ return;
10858
+ }
10859
+ this.connections.set(remoteId, connection);
10860
+ connection.on("data", (payload) => {
10861
+ const deliveries = [];
10862
+ for (const handler of this.messageHandlers) {
10863
+ deliveries.push(handler(payload));
10864
+ }
10865
+ return Promise.all(deliveries);
10866
+ });
10867
+ connection.on("close", () => {
10868
+ this.connections.delete(remoteId);
10869
+ });
10870
+ }
10871
+ async connectToPeer(remotePeerId) {
10872
+ if (!remotePeerId || remotePeerId === this.nodeId) {
10873
+ return null;
10874
+ }
10875
+ const existing = this.connections.get(remotePeerId);
10876
+ if (existing && existing.open) {
10877
+ return existing;
10878
+ }
10879
+ if (this.pendingConnections.has(remotePeerId)) {
10880
+ return this.pendingConnections.get(remotePeerId);
10881
+ }
10882
+ if (!this.peer) {
10883
+ throw new Error("PeerJS network adapter has not been started");
10884
+ }
10885
+ const pending = new Promise((resolve, reject) => {
10886
+ const connection = this.peer.connect(remotePeerId, {
10887
+ reliable: true,
10888
+ serialization: "json"
10889
+ });
10890
+ const timeout = setTimeout(() => {
10891
+ reject(new Error(`Unable to connect to peer ${remotePeerId}`));
10892
+ }, this.connectTimeoutMs);
10893
+ connection.on("open", () => {
10894
+ clearTimeout(timeout);
10895
+ this.attachConnectionHandlers(connection);
10896
+ resolve(connection);
10897
+ });
10898
+ connection.on("error", () => {
10899
+ clearTimeout(timeout);
10900
+ reject(new Error(`Unable to connect to peer ${remotePeerId}`));
10901
+ });
10902
+ }).finally(() => {
10903
+ this.pendingConnections.delete(remotePeerId);
10904
+ });
10905
+ this.pendingConnections.set(remotePeerId, pending);
10906
+ return pending;
10907
+ }
10908
+ onMessage(handler) {
10909
+ this.messageHandlers.add(handler);
10910
+ }
10911
+ offMessage(handler) {
10912
+ this.messageHandlers.delete(handler);
10913
+ }
10914
+ async broadcast(message) {
10915
+ if (!this.peer) {
10916
+ throw new Error("PeerJS network adapter has not been started");
10917
+ }
10918
+ const deliveries = [];
10919
+ for (const connection of this.connections.values()) {
10920
+ if (connection.open) {
10921
+ deliveries.push(connection.send(message));
10922
+ }
10923
+ }
10924
+ await Promise.all(deliveries);
10925
+ }
10926
+ getOpenConnectionCount() {
10927
+ return this.listOpenPeerIds().length;
10928
+ }
10929
+ listOpenPeerIds() {
10930
+ const ids = [];
10931
+ for (const [peerId, connection] of this.connections.entries()) {
10932
+ if (connection.open) {
10933
+ ids.push(peerId);
10934
+ }
10935
+ }
10936
+ return ids;
10937
+ }
10938
+ isConnectedTo(remotePeerId) {
10939
+ const connection = this.connections.get(remotePeerId);
10940
+ return Boolean(connection && connection.open);
10941
+ }
10942
+ async stop() {
10943
+ for (const connection of this.connections.values()) {
10944
+ if (typeof connection.close === "function") {
10945
+ connection.close();
10946
+ }
10947
+ }
10948
+ this.connections.clear();
10949
+ this.pendingConnections.clear();
10950
+ if (this.peer && typeof this.peer.destroy === "function") {
10951
+ this.peer.destroy();
10952
+ }
10953
+ this.peer = null;
10954
+ this.nodeId = null;
10955
+ }
10956
+ };
10957
+ function createPeerJSNetworkAdapter2(options = {}) {
10958
+ return new PeerJSNetworkAdapter2(options);
10959
+ }
10960
+ module2.exports = {
10961
+ PeerJSNetworkAdapter: PeerJSNetworkAdapter2,
10962
+ createPeerJSNetworkAdapter: createPeerJSNetworkAdapter2,
10963
+ parsePeerJsServerUrl
10964
+ };
10965
+ }
10966
+ });
10967
+
10455
10968
  // src/persistence/indexeddb-persistence.js
10456
10969
  var require_indexeddb_persistence = __commonJS({
10457
10970
  "src/persistence/indexeddb-persistence.js"(exports2, module2) {
@@ -10514,6 +11027,7 @@ var require_indexeddb_persistence = __commonJS({
10514
11027
  collection,
10515
11028
  id,
10516
11029
  ownerId: record.ownerId,
11030
+ collaboratorIds: Array.isArray(record.collaboratorIds) ? [...record.collaboratorIds] : [],
10517
11031
  data: { ...record.data },
10518
11032
  createdAt: record.createdAt,
10519
11033
  updatedAt: record.updatedAt,
@@ -10573,6 +11087,7 @@ var require_indexeddb_persistence = __commonJS({
10573
11087
  this.node.restoreRecord(stored.collection, {
10574
11088
  id: stored.id,
10575
11089
  ownerId: stored.ownerId,
11090
+ collaboratorIds: stored.collaboratorIds,
10576
11091
  data: stored.data,
10577
11092
  createdAt: stored.createdAt,
10578
11093
  updatedAt: stored.updatedAt,
@@ -10619,6 +11134,10 @@ var {
10619
11134
  InMemoryNetworkHub,
10620
11135
  InMemoryNetworkAdapter
10621
11136
  } = require_in_memory_network();
11137
+ var {
11138
+ PeerJSNetworkAdapter,
11139
+ createPeerJSNetworkAdapter
11140
+ } = require_peerjs_network();
10622
11141
  var IndexedDBPersistence = require_indexeddb_persistence();
10623
11142
  var {
10624
11143
  DEFAULT_CLOUDFLARE_SIGNALING_URLS,
@@ -10638,6 +11157,8 @@ module.exports = {
10638
11157
  PeerJSSignalingProvider,
10639
11158
  InMemoryNetworkHub,
10640
11159
  InMemoryNetworkAdapter,
11160
+ PeerJSNetworkAdapter,
11161
+ createPeerJSNetworkAdapter,
10641
11162
  IndexedDBPersistence,
10642
11163
  DEFAULT_CLOUDFLARE_SIGNALING_URLS,
10643
11164
  DEFAULT_SIGNALING_FALLBACK_URLS,