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.
- package/README.md +83 -2
- package/dist/dignity.cjs.js +542 -21
- package/dist/dignity.cjs.js.map +4 -4
- package/dist/dignity.esm.js +542 -21
- package/dist/dignity.esm.js.map +3 -3
- package/dist/dignity.min.js +18 -18
- package/docs/assets/dignity.esm.js +11205 -0
- package/docs/assets/favicon.svg +8 -0
- package/docs/chess/assets/chess-app.js +58022 -0
- package/docs/chess/assets/chess-app.js.map +7 -0
- package/docs/chess/assets/chess.css +584 -0
- package/docs/chess/favicon.ico +0 -0
- package/docs/chess/index.html +16 -0
- package/docs/chess/src/App.jsx +128 -0
- package/docs/chess/src/components/Board3D.jsx +364 -0
- package/docs/chess/src/components/GameView.jsx +847 -0
- package/docs/chess/src/components/JoinGate.jsx +68 -0
- package/docs/chess/src/components/LinkPanel.jsx +132 -0
- package/docs/chess/src/components/Lobby.jsx +154 -0
- package/docs/chess/src/components/MovePanel.jsx +123 -0
- package/docs/chess/src/lib/audio.js +50 -0
- package/docs/chess/src/lib/dignitySetup.js +42 -0
- package/docs/chess/src/lib/links.js +124 -0
- package/docs/chess/src/lib/localGames.js +160 -0
- package/docs/chess/src/lib/p2pDebug.js +192 -0
- package/docs/chess/src/main.jsx +5 -0
- package/docs/favicon.ico +0 -0
- package/docs/index.html +7 -3
- package/docs/openapi-like.json +35 -6
- package/examples/decentralized-chess-lite.js +52 -30
- package/package.json +13 -5
- package/src/core/dignity-p2p.js +388 -16
- package/src/index.js +6 -0
- package/src/network/peerjs-network.js +234 -0
- package/src/persistence/indexeddb-persistence.js +2 -0
- package/src/react/index.js +143 -1
- package/src/signaling/parse-peerjs-url.js +24 -0
- package/src/signaling/peerjs-signaling-provider.js +2 -8
package/dist/dignity.esm.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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,
|