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
@@ -2934,12 +2934,71 @@ var require_dignity_p2p = __commonJS({
2934
2934
  return {
2935
2935
  id: record.id,
2936
2936
  ownerId: record.ownerId,
2937
+ collaboratorIds: Array.isArray(record.collaboratorIds) ? [...record.collaboratorIds] : [],
2937
2938
  createdAt: record.createdAt,
2938
2939
  updatedAt: record.updatedAt,
2939
2940
  version: record.version,
2940
2941
  data: { ...record.data }
2941
2942
  };
2942
2943
  }
2944
+ canUpdateRecord(record, actorId) {
2945
+ if (!record || !actorId) {
2946
+ return false;
2947
+ }
2948
+ if (record.ownerId === actorId) {
2949
+ return true;
2950
+ }
2951
+ return Array.isArray(record.collaboratorIds) && record.collaboratorIds.includes(actorId);
2952
+ }
2953
+ normalizeCollaboratorIds(collaborators) {
2954
+ if (!Array.isArray(collaborators)) {
2955
+ return [];
2956
+ }
2957
+ return [...new Set(collaborators.filter(Boolean))];
2958
+ }
2959
+ getRecordPeerIds(collectionName, id, options = {}) {
2960
+ const record = options.fromRecord || this.getCollection(collectionName).get(id);
2961
+ if (!record) {
2962
+ return [];
2963
+ }
2964
+ const includeSelf = options.includeSelf === true;
2965
+ const peerIds = [record.ownerId, ...record.collaboratorIds || []];
2966
+ return [...new Set(peerIds.filter(Boolean).filter((peerId) => includeSelf || peerId !== this.nodeId))];
2967
+ }
2968
+ resolveReplicationPeers(collectionName, id, options = {}, hints = {}) {
2969
+ if (options.connectToPeers === false) {
2970
+ return void 0;
2971
+ }
2972
+ if (Array.isArray(options.connectToPeers)) {
2973
+ return options.connectToPeers;
2974
+ }
2975
+ const peerIds = /* @__PURE__ */ new Set();
2976
+ if (hints.fromRecord) {
2977
+ for (const peerId of this.getRecordPeerIds(collectionName, id, {
2978
+ fromRecord: hints.fromRecord,
2979
+ includeSelf: true
2980
+ })) {
2981
+ peerIds.add(peerId);
2982
+ }
2983
+ } else if (id) {
2984
+ for (const peerId of this.getRecordPeerIds(collectionName, id, { includeSelf: true })) {
2985
+ peerIds.add(peerId);
2986
+ }
2987
+ }
2988
+ if (Array.isArray(options.collaborators)) {
2989
+ for (const peerId of this.normalizeCollaboratorIds(options.collaborators)) {
2990
+ peerIds.add(peerId);
2991
+ }
2992
+ }
2993
+ if (Array.isArray(hints.extraPeerIds)) {
2994
+ for (const peerId of hints.extraPeerIds) {
2995
+ if (peerId) {
2996
+ peerIds.add(peerId);
2997
+ }
2998
+ }
2999
+ }
3000
+ return [...peerIds].filter((peerId) => peerId && peerId !== this.nodeId);
3001
+ }
2943
3002
  async create(collectionName, data, options = {}) {
2944
3003
  const collection = this.getCollection(collectionName);
2945
3004
  const id = options.id || this.idGenerator();
@@ -2947,6 +3006,7 @@ var require_dignity_p2p = __commonJS({
2947
3006
  throw new Error(`Object ${id} already exists in ${collectionName}`);
2948
3007
  }
2949
3008
  const timestamp = this.now();
3009
+ const collaboratorIds = this.normalizeCollaboratorIds(options.collaborators);
2950
3010
  const operation = {
2951
3011
  opId: this.idGenerator(),
2952
3012
  kind: "create",
@@ -2954,6 +3014,7 @@ var require_dignity_p2p = __commonJS({
2954
3014
  id,
2955
3015
  actorId: this.nodeId,
2956
3016
  ownerId: this.nodeId,
3017
+ collaboratorIds,
2957
3018
  timestamp,
2958
3019
  payload: { ...data }
2959
3020
  };
@@ -2963,6 +3024,9 @@ var require_dignity_p2p = __commonJS({
2963
3024
  messageType: "operation",
2964
3025
  operation,
2965
3026
  collectionName
3027
+ }),
3028
+ connectToPeers: this.resolveReplicationPeers(collectionName, null, options, {
3029
+ extraPeerIds: options.collaborators
2966
3030
  })
2967
3031
  });
2968
3032
  return this.read(collectionName, id);
@@ -2997,8 +3061,11 @@ var require_dignity_p2p = __commonJS({
2997
3061
  if (!existing || existing.deletedAt) {
2998
3062
  throw new Error(`Object ${id} does not exist in ${collectionName}`);
2999
3063
  }
3000
- if (existing.ownerId !== this.nodeId) {
3001
- throw new Error(`Only owner ${existing.ownerId} can update object ${id}`);
3064
+ if (!this.canUpdateRecord(existing, this.nodeId)) {
3065
+ throw new Error(`Only owner ${existing.ownerId} or collaborators can update object ${id}`);
3066
+ }
3067
+ if (options.collaborators !== void 0 && existing.ownerId !== this.nodeId) {
3068
+ throw new Error(`Only owner ${existing.ownerId} can change collaborators on object ${id}`);
3002
3069
  }
3003
3070
  if (typeof options.expectedVersion === "number" && existing.version !== options.expectedVersion) {
3004
3071
  this.emitConflict({
@@ -3025,13 +3092,17 @@ var require_dignity_p2p = __commonJS({
3025
3092
  baseVersion: existing.version,
3026
3093
  payload: { ...partialData }
3027
3094
  };
3095
+ if (options.collaborators !== void 0) {
3096
+ operation.collaboratorIds = this.normalizeCollaboratorIds(options.collaborators);
3097
+ }
3028
3098
  this.applyOperation(operation);
3029
3099
  await this.broadcastMessage("operation", operation, {
3030
3100
  broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
3031
3101
  messageType: "operation",
3032
3102
  operation,
3033
3103
  collectionName
3034
- })
3104
+ }),
3105
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, { fromRecord: existing })
3035
3106
  });
3036
3107
  return this.read(collectionName, id);
3037
3108
  }
@@ -3056,6 +3127,42 @@ var require_dignity_p2p = __commonJS({
3056
3127
  }
3057
3128
  throw new Error(`Unable to update ${collectionName}/${id} after ${maxAttempts} attempts`);
3058
3129
  }
3130
+ async transferOwnership(collectionName, id, newOwnerId, options = {}) {
3131
+ if (!newOwnerId) {
3132
+ throw new Error("newOwnerId is required");
3133
+ }
3134
+ const existing = this.getCollection(collectionName).get(id);
3135
+ if (!existing || existing.deletedAt) {
3136
+ throw new Error(`Object ${id} does not exist in ${collectionName}`);
3137
+ }
3138
+ if (existing.ownerId !== this.nodeId) {
3139
+ throw new Error(`Only owner ${existing.ownerId} can transfer object ${id}`);
3140
+ }
3141
+ const operation = {
3142
+ opId: this.idGenerator(),
3143
+ kind: "transfer-ownership",
3144
+ collectionName,
3145
+ id,
3146
+ actorId: this.nodeId,
3147
+ timestamp: this.now(),
3148
+ baseVersion: existing.version,
3149
+ newOwnerId,
3150
+ keepPreviousOwnerAsCollaborator: options.keepAsCollaborator !== false
3151
+ };
3152
+ this.applyOperation(operation);
3153
+ await this.broadcastMessage("operation", operation, {
3154
+ broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
3155
+ messageType: "operation",
3156
+ operation,
3157
+ collectionName
3158
+ }),
3159
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, {
3160
+ fromRecord: existing,
3161
+ extraPeerIds: [newOwnerId]
3162
+ })
3163
+ });
3164
+ return this.read(collectionName, id);
3165
+ }
3059
3166
  async remove(collectionName, id, options = {}) {
3060
3167
  const existing = this.getCollection(collectionName).get(id);
3061
3168
  if (!existing || existing.deletedAt) {
@@ -3079,16 +3186,74 @@ var require_dignity_p2p = __commonJS({
3079
3186
  messageType: "operation",
3080
3187
  operation,
3081
3188
  collectionName
3082
- })
3189
+ }),
3190
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, { fromRecord: existing })
3083
3191
  });
3084
3192
  }
3085
3193
  registerPeerPublicKey(peerId, publicKey) {
3086
3194
  this.securityService.registerPeerPublicKey(peerId, publicKey);
3087
3195
  }
3196
+ trustPeerPublicKey(peerId, publicKey) {
3197
+ if (!peerId || !publicKey) {
3198
+ return false;
3199
+ }
3200
+ try {
3201
+ this.registerPeerPublicKey(peerId, publicKey);
3202
+ return true;
3203
+ } catch (error) {
3204
+ this.emit("warning", { type: "peer-key-trust-failed", peerId, error });
3205
+ return false;
3206
+ }
3207
+ }
3208
+ trustPeerFromMetadata(peerId, metadata) {
3209
+ if (!metadata || !metadata.publicKey) {
3210
+ return false;
3211
+ }
3212
+ return this.trustPeerPublicKey(peerId, metadata.publicKey);
3213
+ }
3088
3214
  getPublicKey() {
3089
3215
  return this.securityService.getPublicKey();
3090
3216
  }
3217
+ async connectToPeer(peerId) {
3218
+ if (!peerId || peerId === this.nodeId) {
3219
+ return null;
3220
+ }
3221
+ if (typeof this.networkAdapter.connectToPeer !== "function") {
3222
+ throw new Error("Network adapter does not support connectToPeer");
3223
+ }
3224
+ return this.networkAdapter.connectToPeer(peerId);
3225
+ }
3226
+ getConnectionStats() {
3227
+ const adapter = this.networkAdapter;
3228
+ if (!adapter) {
3229
+ return { openCount: 0, peerIds: [] };
3230
+ }
3231
+ const peerIds = typeof adapter.listOpenPeerIds === "function" ? adapter.listOpenPeerIds() : [];
3232
+ const openCount = typeof adapter.getOpenConnectionCount === "function" ? adapter.getOpenConnectionCount() : peerIds.length;
3233
+ return { openCount, peerIds };
3234
+ }
3235
+ async ensureConnectedToPeers(peerIds = []) {
3236
+ const normalized = [...new Set((peerIds || []).filter(Boolean))];
3237
+ const results = [];
3238
+ for (const peerId of normalized) {
3239
+ if (peerId === this.nodeId) {
3240
+ continue;
3241
+ }
3242
+ try {
3243
+ await this.connectToPeer(peerId);
3244
+ results.push({ peerId, ok: true });
3245
+ } catch (error) {
3246
+ this.emit("warning", { type: "peer-connect-failed", peerId, error });
3247
+ results.push({ peerId, ok: false, error });
3248
+ }
3249
+ }
3250
+ return results;
3251
+ }
3091
3252
  async broadcastMessage(messageType, payload, securityContext = {}) {
3253
+ const connectToPeers = securityContext.connectToPeers;
3254
+ if (Array.isArray(connectToPeers) && connectToPeers.length > 0) {
3255
+ await this.ensureConnectedToPeers(connectToPeers);
3256
+ }
3092
3257
  const envelope = await this.securityService.secureOutgoingMessage({
3093
3258
  messageType,
3094
3259
  payload,
@@ -3098,6 +3263,13 @@ var require_dignity_p2p = __commonJS({
3098
3263
  await this.networkAdapter.broadcast(envelope);
3099
3264
  }
3100
3265
  async sendDirectMessage(targetId, messageType, payload) {
3266
+ if (targetId) {
3267
+ try {
3268
+ await this.connectToPeer(targetId);
3269
+ } catch (error) {
3270
+ this.emit("warning", { type: "direct-message-connect-failed", targetId, error });
3271
+ }
3272
+ }
3101
3273
  const envelope = await this.securityService.secureOutgoingMessage({
3102
3274
  messageType,
3103
3275
  payload,
@@ -3122,6 +3294,7 @@ var require_dignity_p2p = __commonJS({
3122
3294
  expiresAt: announcedAt + ttlMs
3123
3295
  };
3124
3296
  map.set(peerId, next);
3297
+ this.trustPeerFromMetadata(peerId, next.metadata);
3125
3298
  if (!existing) {
3126
3299
  this.emit("peerdiscovered", { scope, peerId, metadata: next.metadata });
3127
3300
  }
@@ -3144,11 +3317,18 @@ var require_dignity_p2p = __commonJS({
3144
3317
  const normalizedScope = scope || "main";
3145
3318
  const heartbeatIntervalMs = options.heartbeatIntervalMs || this.defaultDiscoveryHeartbeatMs;
3146
3319
  const ttlMs = options.ttlMs || this.defaultPresenceTtlMs;
3147
- const metadata = options.metadata || {};
3320
+ const metadata = {
3321
+ publicKey: this.getPublicKey(),
3322
+ ...options.metadata || {}
3323
+ };
3324
+ const bootstrapPeerIds = Array.isArray(options.bootstrapPeerIds) ? [...new Set(options.bootstrapPeerIds.filter(Boolean))] : [];
3148
3325
  const existing = this.discoveryRooms.get(normalizedScope);
3149
3326
  if (existing && existing.timer) {
3150
3327
  clearInterval(existing.timer);
3151
3328
  }
3329
+ if (bootstrapPeerIds.length > 0) {
3330
+ await this.ensureConnectedToPeers(bootstrapPeerIds);
3331
+ }
3152
3332
  const timer = setInterval(() => {
3153
3333
  this.announcePresence(normalizedScope).catch((error) => {
3154
3334
  this.emit("warning", { type: "presence-heartbeat-failed", scope: normalizedScope, error });
@@ -3156,6 +3336,7 @@ var require_dignity_p2p = __commonJS({
3156
3336
  }, heartbeatIntervalMs);
3157
3337
  this.discoveryRooms.set(normalizedScope, {
3158
3338
  metadata,
3339
+ bootstrapPeerIds,
3159
3340
  heartbeatIntervalMs,
3160
3341
  ttlMs,
3161
3342
  timer
@@ -3236,6 +3417,9 @@ var require_dignity_p2p = __commonJS({
3236
3417
  });
3237
3418
  return;
3238
3419
  }
3420
+ if (message && message.senderId && message.senderPublicKey) {
3421
+ this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
3422
+ }
3239
3423
  let decrypted;
3240
3424
  try {
3241
3425
  decrypted = await this.securityService.decryptIncomingMessage(message);
@@ -3257,6 +3441,21 @@ var require_dignity_p2p = __commonJS({
3257
3441
  this.applyOperation(decrypted.payload);
3258
3442
  return;
3259
3443
  }
3444
+ if (decrypted.messageType === "record:snapshot") {
3445
+ const payload = decrypted.payload || {};
3446
+ const { collectionName, record } = payload;
3447
+ if (collectionName && record) {
3448
+ const applied = this.restoreRecord(collectionName, record);
3449
+ if (applied) {
3450
+ this.emit("change", {
3451
+ kind: "snapshot",
3452
+ collection: collectionName,
3453
+ id: record.id
3454
+ });
3455
+ }
3456
+ }
3457
+ return;
3458
+ }
3260
3459
  if (decrypted.messageType === "presence:announce") {
3261
3460
  const payload = decrypted.payload || {};
3262
3461
  const scope = payload.scope || "main";
@@ -3274,6 +3473,11 @@ var require_dignity_p2p = __commonJS({
3274
3473
  payload.announcedAt || this.now()
3275
3474
  );
3276
3475
  if (isNewPeerInScope && peerId !== this.nodeId && this.discoveryRooms.has(scope)) {
3476
+ if (typeof this.networkAdapter.connectToPeer === "function") {
3477
+ Promise.resolve(this.connectToPeer(peerId)).catch((error) => {
3478
+ this.emit("warning", { type: "peer-connect-failed", scope, peerId, error });
3479
+ });
3480
+ }
3277
3481
  this.announcePresence(scope).catch((error) => {
3278
3482
  this.emit("warning", { type: "presence-handshake-failed", scope, error });
3279
3483
  });
@@ -3348,6 +3552,7 @@ var require_dignity_p2p = __commonJS({
3348
3552
  collection.set(record.id, {
3349
3553
  id: record.id,
3350
3554
  ownerId: record.ownerId,
3555
+ collaboratorIds: this.normalizeCollaboratorIds(record.collaboratorIds),
3351
3556
  data: { ...record.data || {} },
3352
3557
  createdAt: record.createdAt,
3353
3558
  updatedAt: record.updatedAt,
@@ -3356,6 +3561,32 @@ var require_dignity_p2p = __commonJS({
3356
3561
  });
3357
3562
  return true;
3358
3563
  }
3564
+ async pushRecordSnapshot(collectionName, id, options = {}) {
3565
+ const collection = this.getCollection(collectionName);
3566
+ const raw = collection.get(id);
3567
+ if (!raw || raw.deletedAt) {
3568
+ throw new Error(`Object ${id} does not exist in ${collectionName}`);
3569
+ }
3570
+ const record = {
3571
+ id: raw.id,
3572
+ ownerId: raw.ownerId,
3573
+ collaboratorIds: Array.isArray(raw.collaboratorIds) ? [...raw.collaboratorIds] : [],
3574
+ data: { ...raw.data },
3575
+ createdAt: raw.createdAt,
3576
+ updatedAt: raw.updatedAt,
3577
+ deletedAt: raw.deletedAt || null,
3578
+ version: raw.version
3579
+ };
3580
+ await this.broadcastMessage("record:snapshot", { collectionName, record }, {
3581
+ broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
3582
+ messageType: "record:snapshot",
3583
+ collectionName,
3584
+ id
3585
+ }),
3586
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, { fromRecord: raw })
3587
+ });
3588
+ return record;
3589
+ }
3359
3590
  applyOperation(operation) {
3360
3591
  if (!operation || !operation.opId || this.appliedOperations.has(operation.opId)) {
3361
3592
  return false;
@@ -3369,6 +3600,7 @@ var require_dignity_p2p = __commonJS({
3369
3600
  collection.set(operation.id, {
3370
3601
  id: operation.id,
3371
3602
  ownerId: operation.ownerId,
3603
+ collaboratorIds: this.normalizeCollaboratorIds(operation.collaboratorIds),
3372
3604
  data: { ...operation.payload },
3373
3605
  createdAt: operation.timestamp,
3374
3606
  updatedAt: operation.timestamp,
@@ -3380,9 +3612,79 @@ var require_dignity_p2p = __commonJS({
3380
3612
  return true;
3381
3613
  }
3382
3614
  if (!current || current.deletedAt) {
3615
+ if (operation.kind !== "create") {
3616
+ this.emit("warning", {
3617
+ type: "orphan-operation",
3618
+ kind: operation.kind,
3619
+ collection: operation.collectionName,
3620
+ id: operation.id,
3621
+ actorId: operation.actorId,
3622
+ hint: "Peer is missing the record; pushRecordSnapshot from the owner to catch up."
3623
+ });
3624
+ }
3383
3625
  return false;
3384
3626
  }
3385
- if (operation.actorId !== current.ownerId) {
3627
+ if (operation.kind === "transfer-ownership") {
3628
+ if (operation.actorId !== current.ownerId) {
3629
+ return false;
3630
+ }
3631
+ if (typeof operation.baseVersion === "number" && operation.baseVersion !== current.version) {
3632
+ this.emitConflict({
3633
+ kind: operation.kind,
3634
+ collection: operation.collectionName,
3635
+ id: operation.id,
3636
+ expectedVersion: operation.baseVersion,
3637
+ currentVersion: current.version,
3638
+ phase: "remote",
3639
+ operation
3640
+ });
3641
+ return false;
3642
+ }
3643
+ const previousOwnerId = current.ownerId;
3644
+ current.ownerId = operation.newOwnerId;
3645
+ if (operation.keepPreviousOwnerAsCollaborator !== false) {
3646
+ const collaborators = this.normalizeCollaboratorIds(current.collaboratorIds);
3647
+ if (!collaborators.includes(previousOwnerId)) {
3648
+ collaborators.push(previousOwnerId);
3649
+ }
3650
+ current.collaboratorIds = collaborators.filter((peerId) => peerId !== operation.newOwnerId);
3651
+ }
3652
+ current.updatedAt = operation.timestamp;
3653
+ current.version += 1;
3654
+ this.appliedOperations.add(operation.opId);
3655
+ this.emit("change", {
3656
+ kind: "transfer-ownership",
3657
+ collection: operation.collectionName,
3658
+ id: operation.id,
3659
+ previousOwnerId,
3660
+ newOwnerId: operation.newOwnerId
3661
+ });
3662
+ return true;
3663
+ }
3664
+ if (operation.kind === "delete") {
3665
+ if (operation.actorId !== current.ownerId) {
3666
+ return false;
3667
+ }
3668
+ if (typeof operation.baseVersion === "number" && operation.baseVersion !== current.version) {
3669
+ this.emitConflict({
3670
+ kind: operation.kind,
3671
+ collection: operation.collectionName,
3672
+ id: operation.id,
3673
+ expectedVersion: operation.baseVersion,
3674
+ currentVersion: current.version,
3675
+ phase: "remote",
3676
+ operation
3677
+ });
3678
+ return false;
3679
+ }
3680
+ current.deletedAt = operation.timestamp;
3681
+ current.updatedAt = operation.timestamp;
3682
+ current.version += 1;
3683
+ this.appliedOperations.add(operation.opId);
3684
+ this.emit("change", { kind: "delete", collection: operation.collectionName, id: operation.id });
3685
+ return true;
3686
+ }
3687
+ if (!this.canUpdateRecord(current, operation.actorId)) {
3386
3688
  return false;
3387
3689
  }
3388
3690
  if (typeof operation.baseVersion === "number" && operation.baseVersion !== current.version) {
@@ -3402,20 +3704,15 @@ var require_dignity_p2p = __commonJS({
3402
3704
  ...current.data,
3403
3705
  ...operation.payload
3404
3706
  };
3707
+ if (Array.isArray(operation.collaboratorIds) && operation.actorId === current.ownerId) {
3708
+ current.collaboratorIds = this.normalizeCollaboratorIds(operation.collaboratorIds);
3709
+ }
3405
3710
  current.updatedAt = operation.timestamp;
3406
3711
  current.version += 1;
3407
3712
  this.appliedOperations.add(operation.opId);
3408
3713
  this.emit("change", { kind: "update", collection: operation.collectionName, id: operation.id });
3409
3714
  return true;
3410
3715
  }
3411
- if (operation.kind === "delete") {
3412
- current.deletedAt = operation.timestamp;
3413
- current.updatedAt = operation.timestamp;
3414
- current.version += 1;
3415
- this.appliedOperations.add(operation.opId);
3416
- this.emit("change", { kind: "delete", collection: operation.collectionName, id: operation.id });
3417
- return true;
3418
- }
3419
3716
  return false;
3420
3717
  }
3421
3718
  };
@@ -3570,6 +3867,28 @@ var require_websocket_signaling_provider = __commonJS({
3570
3867
  }
3571
3868
  });
3572
3869
 
3870
+ // src/signaling/parse-peerjs-url.js
3871
+ var require_parse_peerjs_url = __commonJS({
3872
+ "src/signaling/parse-peerjs-url.js"(exports, module) {
3873
+ function parsePeerJsServerUrl(url) {
3874
+ const parsed = new URL(url);
3875
+ const secure = parsed.protocol === "wss:";
3876
+ const host = parsed.hostname;
3877
+ const port = parsed.port ? Number(parsed.port) : secure ? 443 : 80;
3878
+ const key = parsed.searchParams.get("key") || "peerjs";
3879
+ let path = parsed.pathname || "/";
3880
+ if (path.endsWith("/peerjs")) {
3881
+ path = path.slice(0, -"/peerjs".length) || "/";
3882
+ }
3883
+ if (path !== "/" && !path.endsWith("/")) {
3884
+ path += "/";
3885
+ }
3886
+ return { secure, host, port, path, key };
3887
+ }
3888
+ module.exports = parsePeerJsServerUrl;
3889
+ }
3890
+ });
3891
+
3573
3892
  // node_modules/peerjs-js-binarypack/dist/binarypack.cjs
3574
3893
  var require_binarypack = __commonJS({
3575
3894
  "node_modules/peerjs-js-binarypack/dist/binarypack.cjs"(exports, module) {
@@ -10169,6 +10488,7 @@ var require_bundler = __commonJS({
10169
10488
  var require_peerjs_signaling_provider = __commonJS({
10170
10489
  "src/signaling/peerjs-signaling-provider.js"(exports, module) {
10171
10490
  var WebSocketSignalingProvider = require_websocket_signaling_provider();
10491
+ var parsePeerJsServerUrl = require_parse_peerjs_url();
10172
10492
  var PeerJSSignalingProvider = class {
10173
10493
  constructor({ id, url, PeerImpl, WebSocketImpl, priority = 0, connectTimeoutMs = 1e4 }) {
10174
10494
  if (!url) {
@@ -10196,13 +10516,7 @@ var require_peerjs_signaling_provider = __commonJS({
10196
10516
  }
10197
10517
  }
10198
10518
  parsePeerJsServerUrl() {
10199
- const parsed = new URL(this.url);
10200
- const secure = parsed.protocol === "wss:";
10201
- const host = parsed.hostname;
10202
- const port = parsed.port ? Number(parsed.port) : secure ? 443 : 80;
10203
- const path = parsed.pathname || "/";
10204
- const key = parsed.searchParams.get("key") || "peerjs";
10205
- return { secure, host, port, path, key };
10519
+ return parsePeerJsServerUrl(this.url);
10206
10520
  }
10207
10521
  shouldUseWebSocketFallback() {
10208
10522
  return !this.isCustomPeerImpl && typeof globalThis.RTCPeerConnection !== "function";
@@ -10482,6 +10796,205 @@ var require_in_memory_network = __commonJS({
10482
10796
  }
10483
10797
  });
10484
10798
 
10799
+ // src/network/peerjs-network.js
10800
+ var require_peerjs_network = __commonJS({
10801
+ "src/network/peerjs-network.js"(exports, module) {
10802
+ var { DEFAULT_CLOUDFLARE_SIGNALING_URLS } = require_default_signaling_config();
10803
+ var parsePeerJsServerUrl = require_parse_peerjs_url();
10804
+ function resolvePeerImplementation(PeerImpl) {
10805
+ if (PeerImpl) {
10806
+ return PeerImpl;
10807
+ }
10808
+ try {
10809
+ const peerjs = require_bundler();
10810
+ return peerjs.Peer || peerjs;
10811
+ } catch (error) {
10812
+ return null;
10813
+ }
10814
+ }
10815
+ var PeerJSNetworkAdapter = class {
10816
+ constructor({
10817
+ url,
10818
+ urls,
10819
+ PeerImpl,
10820
+ connectTimeoutMs = 12e3
10821
+ } = {}) {
10822
+ this.urls = urls || (url ? [url] : [...DEFAULT_CLOUDFLARE_SIGNALING_URLS]);
10823
+ this.url = this.urls[0];
10824
+ this.PeerImpl = resolvePeerImplementation(PeerImpl);
10825
+ this.connectTimeoutMs = connectTimeoutMs;
10826
+ this.nodeId = null;
10827
+ this.peer = null;
10828
+ this.connections = /* @__PURE__ */ new Map();
10829
+ this.pendingConnections = /* @__PURE__ */ new Map();
10830
+ this.messageHandlers = /* @__PURE__ */ new Set();
10831
+ }
10832
+ async start(nodeId) {
10833
+ if (!nodeId) {
10834
+ throw new Error("PeerJSNetworkAdapter requires nodeId on start");
10835
+ }
10836
+ if (!this.PeerImpl) {
10837
+ throw new Error("PeerJS implementation is not available");
10838
+ }
10839
+ if (this.peer) {
10840
+ await this.stop();
10841
+ }
10842
+ let lastError;
10843
+ for (const candidateUrl of this.urls) {
10844
+ try {
10845
+ await this.startWithUrl(nodeId, candidateUrl);
10846
+ this.url = candidateUrl;
10847
+ return;
10848
+ } catch (error) {
10849
+ lastError = error;
10850
+ }
10851
+ }
10852
+ throw lastError || new Error("Unable to connect PeerJS network adapter");
10853
+ }
10854
+ async startWithUrl(nodeId, url) {
10855
+ this.nodeId = nodeId;
10856
+ const server = parsePeerJsServerUrl(url);
10857
+ await new Promise((resolve, reject) => {
10858
+ const peer = new this.PeerImpl(nodeId, {
10859
+ host: server.host,
10860
+ port: server.port,
10861
+ path: server.path,
10862
+ secure: server.secure,
10863
+ key: server.key
10864
+ });
10865
+ const timeout = setTimeout(() => {
10866
+ peer.destroy?.();
10867
+ reject(new Error(`Unable to connect PeerJS network adapter to ${url}`));
10868
+ }, this.connectTimeoutMs);
10869
+ peer.on("open", () => {
10870
+ clearTimeout(timeout);
10871
+ this.peer = peer;
10872
+ resolve();
10873
+ });
10874
+ peer.on("connection", (connection) => {
10875
+ this.attachConnectionHandlers(connection);
10876
+ });
10877
+ peer.on("error", (error) => {
10878
+ clearTimeout(timeout);
10879
+ peer.destroy?.();
10880
+ reject(error || new Error(`Unable to connect PeerJS network adapter to ${url}`));
10881
+ });
10882
+ });
10883
+ }
10884
+ attachConnectionHandlers(connection) {
10885
+ const remoteId = connection.peer;
10886
+ if (!remoteId) {
10887
+ return;
10888
+ }
10889
+ this.connections.set(remoteId, connection);
10890
+ connection.on("data", (payload) => {
10891
+ const deliveries = [];
10892
+ for (const handler of this.messageHandlers) {
10893
+ deliveries.push(handler(payload));
10894
+ }
10895
+ return Promise.all(deliveries);
10896
+ });
10897
+ connection.on("close", () => {
10898
+ this.connections.delete(remoteId);
10899
+ });
10900
+ }
10901
+ async connectToPeer(remotePeerId) {
10902
+ if (!remotePeerId || remotePeerId === this.nodeId) {
10903
+ return null;
10904
+ }
10905
+ const existing = this.connections.get(remotePeerId);
10906
+ if (existing && existing.open) {
10907
+ return existing;
10908
+ }
10909
+ if (this.pendingConnections.has(remotePeerId)) {
10910
+ return this.pendingConnections.get(remotePeerId);
10911
+ }
10912
+ if (!this.peer) {
10913
+ throw new Error("PeerJS network adapter has not been started");
10914
+ }
10915
+ const pending = new Promise((resolve, reject) => {
10916
+ const connection = this.peer.connect(remotePeerId, {
10917
+ reliable: true,
10918
+ serialization: "json"
10919
+ });
10920
+ const timeout = setTimeout(() => {
10921
+ reject(new Error(`Unable to connect to peer ${remotePeerId}`));
10922
+ }, this.connectTimeoutMs);
10923
+ connection.on("open", () => {
10924
+ clearTimeout(timeout);
10925
+ this.attachConnectionHandlers(connection);
10926
+ resolve(connection);
10927
+ });
10928
+ connection.on("error", () => {
10929
+ clearTimeout(timeout);
10930
+ reject(new Error(`Unable to connect to peer ${remotePeerId}`));
10931
+ });
10932
+ }).finally(() => {
10933
+ this.pendingConnections.delete(remotePeerId);
10934
+ });
10935
+ this.pendingConnections.set(remotePeerId, pending);
10936
+ return pending;
10937
+ }
10938
+ onMessage(handler) {
10939
+ this.messageHandlers.add(handler);
10940
+ }
10941
+ offMessage(handler) {
10942
+ this.messageHandlers.delete(handler);
10943
+ }
10944
+ async broadcast(message) {
10945
+ if (!this.peer) {
10946
+ throw new Error("PeerJS network adapter has not been started");
10947
+ }
10948
+ const deliveries = [];
10949
+ for (const connection of this.connections.values()) {
10950
+ if (connection.open) {
10951
+ deliveries.push(connection.send(message));
10952
+ }
10953
+ }
10954
+ await Promise.all(deliveries);
10955
+ }
10956
+ getOpenConnectionCount() {
10957
+ return this.listOpenPeerIds().length;
10958
+ }
10959
+ listOpenPeerIds() {
10960
+ const ids = [];
10961
+ for (const [peerId, connection] of this.connections.entries()) {
10962
+ if (connection.open) {
10963
+ ids.push(peerId);
10964
+ }
10965
+ }
10966
+ return ids;
10967
+ }
10968
+ isConnectedTo(remotePeerId) {
10969
+ const connection = this.connections.get(remotePeerId);
10970
+ return Boolean(connection && connection.open);
10971
+ }
10972
+ async stop() {
10973
+ for (const connection of this.connections.values()) {
10974
+ if (typeof connection.close === "function") {
10975
+ connection.close();
10976
+ }
10977
+ }
10978
+ this.connections.clear();
10979
+ this.pendingConnections.clear();
10980
+ if (this.peer && typeof this.peer.destroy === "function") {
10981
+ this.peer.destroy();
10982
+ }
10983
+ this.peer = null;
10984
+ this.nodeId = null;
10985
+ }
10986
+ };
10987
+ function createPeerJSNetworkAdapter(options = {}) {
10988
+ return new PeerJSNetworkAdapter(options);
10989
+ }
10990
+ module.exports = {
10991
+ PeerJSNetworkAdapter,
10992
+ createPeerJSNetworkAdapter,
10993
+ parsePeerJsServerUrl
10994
+ };
10995
+ }
10996
+ });
10997
+
10485
10998
  // src/persistence/indexeddb-persistence.js
10486
10999
  var require_indexeddb_persistence = __commonJS({
10487
11000
  "src/persistence/indexeddb-persistence.js"(exports, module) {
@@ -10544,6 +11057,7 @@ var require_indexeddb_persistence = __commonJS({
10544
11057
  collection,
10545
11058
  id,
10546
11059
  ownerId: record.ownerId,
11060
+ collaboratorIds: Array.isArray(record.collaboratorIds) ? [...record.collaboratorIds] : [],
10547
11061
  data: { ...record.data },
10548
11062
  createdAt: record.createdAt,
10549
11063
  updatedAt: record.updatedAt,
@@ -10603,6 +11117,7 @@ var require_indexeddb_persistence = __commonJS({
10603
11117
  this.node.restoreRecord(stored.collection, {
10604
11118
  id: stored.id,
10605
11119
  ownerId: stored.ownerId,
11120
+ collaboratorIds: stored.collaboratorIds,
10606
11121
  data: stored.data,
10607
11122
  createdAt: stored.createdAt,
10608
11123
  updatedAt: stored.updatedAt,
@@ -10651,6 +11166,10 @@ var require_index = __commonJS({
10651
11166
  InMemoryNetworkHub,
10652
11167
  InMemoryNetworkAdapter
10653
11168
  } = require_in_memory_network();
11169
+ var {
11170
+ PeerJSNetworkAdapter,
11171
+ createPeerJSNetworkAdapter
11172
+ } = require_peerjs_network();
10654
11173
  var IndexedDBPersistence = require_indexeddb_persistence();
10655
11174
  var {
10656
11175
  DEFAULT_CLOUDFLARE_SIGNALING_URLS,
@@ -10670,6 +11189,8 @@ var require_index = __commonJS({
10670
11189
  PeerJSSignalingProvider,
10671
11190
  InMemoryNetworkHub,
10672
11191
  InMemoryNetworkAdapter,
11192
+ PeerJSNetworkAdapter,
11193
+ createPeerJSNetworkAdapter,
10673
11194
  IndexedDBPersistence,
10674
11195
  DEFAULT_CLOUDFLARE_SIGNALING_URLS,
10675
11196
  DEFAULT_SIGNALING_FALLBACK_URLS,