@kubun/plugin-p2p 0.8.0 → 0.8.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/lib/context/group.d.ts +4 -0
- package/lib/context/group.js +1 -0
- package/lib/context/join.js +1 -1
- package/lib/context/sync.js +1 -1
- package/lib/context/types.d.ts +7 -8
- package/lib/groups/broadcast-codec.d.ts +3 -0
- package/lib/groups/broadcast-service.d.ts +76 -0
- package/lib/groups/broadcast-service.js +1 -0
- package/lib/{spaces → groups}/broadcast.d.ts +9 -10
- package/lib/groups/broadcast.js +1 -0
- package/lib/groups/events.d.ts +38 -0
- package/lib/{spaces → groups}/events.js +1 -1
- package/lib/{spaces → groups}/invite-payload.d.ts +2 -2
- package/lib/groups/join-utils.d.ts +19 -0
- package/lib/groups/join-utils.js +1 -0
- package/lib/{spaces → groups}/manager.d.ts +56 -46
- package/lib/groups/manager.js +1 -0
- package/lib/groups/mls-group-handle.d.ts +4 -0
- package/lib/groups/mls-group-handle.js +1 -0
- package/lib/groups/mls-state.d.ts +42 -0
- package/lib/groups/mls-state.js +1 -0
- package/lib/hub/connection-pool.d.ts +19 -0
- package/lib/hub/connection-pool.js +1 -0
- package/lib/hub/group-channel.d.ts +51 -0
- package/lib/hub/group-channel.js +1 -0
- package/lib/hub/receive-handler.d.ts +36 -0
- package/lib/hub/receive-handler.js +1 -0
- package/lib/hub/relay-manager.d.ts +37 -0
- package/lib/hub/relay-manager.js +1 -0
- package/lib/hub/send-handler.d.ts +28 -0
- package/lib/hub/send-handler.js +1 -0
- package/lib/index.d.ts +14 -13
- package/lib/index.js +1 -1
- package/lib/protocol.d.ts +44 -10
- package/lib/protocol.js +1 -1
- package/lib/schema.d.ts +2 -2
- package/lib/schema.js +42 -42
- package/lib/sync/catalog-scope.js +1 -1
- package/lib/sync/handlers.d.ts +10 -0
- package/lib/sync/handlers.js +1 -1
- package/lib/sync/merkle-apply.js +1 -1
- package/lib/sync/merkle-channel.d.ts +18 -0
- package/lib/sync/merkle-channel.js +1 -0
- package/lib/sync/merkle-tree.d.ts +3 -1
- package/lib/sync/merkle-tree.js +1 -1
- package/lib/sync/peer-registry.d.ts +27 -8
- package/lib/sync/peer-registry.js +1 -1
- package/lib/sync/sync-client.d.ts +15 -13
- package/lib/sync/sync-client.js +1 -1
- package/lib/sync/sync-manager.d.ts +36 -11
- package/lib/sync/sync-manager.js +1 -1
- package/lib/types.d.ts +42 -40
- package/package.json +20 -16
- package/lib/context/space.d.ts +0 -4
- package/lib/context/space.js +0 -1
- package/lib/migrations.d.ts +0 -8
- package/lib/migrations.js +0 -1
- package/lib/spaces/broadcast-codec.d.ts +0 -3
- package/lib/spaces/broadcast-service.d.ts +0 -59
- package/lib/spaces/broadcast-service.js +0 -1
- package/lib/spaces/broadcast.js +0 -1
- package/lib/spaces/events.d.ts +0 -38
- package/lib/spaces/group-handle.d.ts +0 -3
- package/lib/spaces/group-handle.js +0 -1
- package/lib/spaces/join-utils.d.ts +0 -22
- package/lib/spaces/join-utils.js +0 -1
- package/lib/spaces/manager.js +0 -1
- package/lib/spaces/mls-state.d.ts +0 -27
- package/lib/spaces/mls-state.js +0 -1
- /package/lib/{spaces → groups}/broadcast-codec.js +0 -0
- /package/lib/{spaces → groups}/invite-payload.js +0 -0
- /package/lib/{spaces → groups}/mls-json.d.ts +0 -0
- /package/lib/{spaces → groups}/mls-json.js +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{handleReceivedMessage as e}from"./receive-handler.js";import{handleSendBroadcast as t}from"./send-handler.js";export function createMutex(){let e=Promise.resolve();return{run(t){let r=e.then(()=>t());return e=r.catch(()=>{}),r}}}export class AckBatch{#e;#t;#r;#i;#s=new Set;#o=null;constructor(e){this.#e=e.flushMs,this.#t=e.flushMax,this.#r=e.send,this.#i=e.logger}add(e){let t=0===this.#s.size;if(this.#s.add(e),this.#s.size>=this.#t){null!==this.#o&&(clearTimeout(this.#o),this.#o=null),this.flush();return}t&&null===this.#o&&(this.#o=setTimeout(()=>{this.#o=null,this.flush()},this.#e))}async flush(){if(null!==this.#o&&(clearTimeout(this.#o),this.#o=null),0===this.#s.size)return;let e=Array.from(this.#s);this.#s.clear();try{await this.#r(e)}catch(t){this.#i?.error("ack batch send failed",{error:t,count:e.length})}}dispose(){null!==this.#o&&(clearTimeout(this.#o),this.#o=null),this.#s.clear()}}export function computeBackoff(e,t,r,i){let s=Math.min(t*2**e*(1+i*(2*Math.random()-1)),r);return s<0?0:s}export function abortableSleep(e,t){return new Promise((r,i)=>{if(t.aborted)return void i(t.reason??Error("aborted"));let s=setTimeout(()=>{t.removeEventListener("abort",o),r()},e),o=()=>{clearTimeout(s),t.removeEventListener("abort",o),i(t.reason??Error("aborted"))};t.addEventListener("abort",o)})}let r=Symbol("disposed");export class GroupChannel{#a;#h;#l;#n;#c;#u;#i;#p;#g;#d;#f;#b;#I;#D=new AbortController;#m=null;#k=null;#v=null;#M=null;#y=null;#w=0;#x=!1;#C=!1;constructor(e){this.#a=e.groupID,this.#h=e.hubURL,this.#l=e.deviceID,this.#n=e.pool,this.#c=e.broadcastService,this.#u=e.p2pStore,this.#i=e.logger,this.#p=e.ackFlushMs,this.#g=e.ackFlushMax,this.#d=e.backoffBaseMs,this.#f=e.backoffMaxMs,this.#b=e.backoffJitter,this.#I=createMutex()}async open(){if(this.#x)throw Error("GroupChannel already opened");try{await this.#S(),this.#x=!0}catch(e){if(null!=this.#k){try{await this.#n.release(this.#h,this.#k)}catch(e){this.#i?.debug("pool release after open failure failed",{groupID:this.#a,error:e})}this.#k=null}throw this.#m=null,e}}async #S(){if(null!=this.#k){let e=this.#k;this.#k=null,this.#m=null;try{await this.#n.release(this.#h,e)}catch(e){this.#i?.debug("pool release before reconnect failed",{groupID:this.#a,error:e})}}let{client:e,refID:t}=await this.#n.acquire(this.#h);this.#m=e,this.#k=t;let r=await this.#u.getMLSState(this.#a,this.#l);if(null==r)throw Error(`MLS state missing for group ${this.#a}; cannot join hub group`);await e.request("hub/group/join",{param:{groupID:this.#a,credential:r.credential}});let i=e.createChannel("hub/receive",{param:{groupIDs:[this.#a]},signal:this.#D.signal});this.#v=i,i.catch(()=>{}),this.#M=new AckBatch({flushMs:this.#p,flushMax:this.#g,send:async e=>{await i.send({ack:e})},logger:this.#i}),this.#y=this.#L(e).catch(e=>{this.#i?.debug("receive task tail error",{groupID:this.#a,error:e})}),this.#C=!0}async #L(t){let i=this.#v;if(null==i)return;let s=i.readable.getReader();try{for(;!this.#D.signal.aborted;){let i=s.read(),o=t.disposed.then(()=>r),a=await Promise.race([i,o]);if("symbol"==typeof a){this.#i?.debug("receive loop observed hub client disposal",{groupID:this.#a});break}let{done:h,value:l}=a;if(h)break;(await this.#I.run(()=>e({groupID:this.#a,deviceID:this.#l,message:l,p2pStore:this.#u,broadcastService:this.#c,logger:this.#i}))).ack&&this.#M?.add(l.sequenceID),this.#w=0}}catch(e){this.#i?.warn("hub receive loop error",{groupID:this.#a,error:e})}finally{try{s.releaseLock()}catch{}}this.#C=!1,this.#D.signal.aborted||this.#B()}#B(){this.#C=!1,this.#y=this.#R().catch(e=>{this.#i?.debug("reconnect task tail error",{groupID:this.#a,error:e})})}async #R(){for(;!this.#D.signal.aborted;){this.#C=!1;let e=computeBackoff(this.#w++,this.#d,this.#f,this.#b);this.#i?.debug("reconnect scheduled",{groupID:this.#a,delay:e,attempt:this.#w});try{await abortableSleep(e,this.#D.signal)}catch{return}try{this.#M?.dispose(),this.#M=null,this.#v=null,await this.#S(),this.#w=0;return}catch(e){this.#i?.warn("reconnect attempt failed",{groupID:this.#a,attempt:this.#w,error:e})}}}async close(){if(!this.#x)return;this.#C=!1;let e=this.#M;this.#M=null;try{await e?.flush()}catch(e){this.#i?.debug("ack flush during close failed",{groupID:this.#a,error:e})}e?.dispose();try{await this.#m?.request("hub/group/leave",{param:{groupID:this.#a}})}catch(e){this.#i?.warn("hub/group/leave failed",{groupID:this.#a,error:e})}try{this.#v?.close()}catch{}if(!this.#D.signal.aborted)try{this.#D.abort()}catch{}try{await this.#y}catch{}if(null!=this.#k){try{await this.#n.release(this.#h,this.#k)}catch(e){this.#i?.debug("pool release during close failed",{groupID:this.#a,error:e})}this.#k=null}this.#m=null,this.#v=null,this.#y=null,this.#x=!1}broadcast(e){return this.#x&&this.#C&&null!=this.#m?this.#I.run(async()=>{if(!this.#x||!this.#C||null==this.#m)throw Error(`GroupChannel for ${this.#a} is not ready for broadcast`);let r=this.#m;return t({groupID:this.#a,deviceID:this.#l,message:e,p2pStore:this.#u,broadcastService:this.#c,hubClient:r,logger:this.#i})}):Promise.reject(Error(`GroupChannel for ${this.#a} is not ready for broadcast`))}}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Logger } from '@kubun/logger';
|
|
2
|
+
import type { P2PStoreAPI } from '@kubun/store-p2p';
|
|
3
|
+
import { type BroadcastService } from '../groups/broadcast-service.js';
|
|
4
|
+
/**
|
|
5
|
+
* Shape of a message pushed by `hub/receive`. Matches `@enkaku/hub-protocol`
|
|
6
|
+
* server-to-client push payload.
|
|
7
|
+
*/
|
|
8
|
+
export type ReceivedHubMessage = {
|
|
9
|
+
sequenceID: string;
|
|
10
|
+
senderDID: string;
|
|
11
|
+
groupID?: string;
|
|
12
|
+
/** base64-encoded encrypted MLS application message */
|
|
13
|
+
payload: string;
|
|
14
|
+
};
|
|
15
|
+
export type HandleReceivedMessageParams = {
|
|
16
|
+
groupID: string;
|
|
17
|
+
deviceID: string;
|
|
18
|
+
message: ReceivedHubMessage;
|
|
19
|
+
p2pStore: P2PStoreAPI;
|
|
20
|
+
broadcastService: BroadcastService;
|
|
21
|
+
logger?: Logger;
|
|
22
|
+
};
|
|
23
|
+
export type HandleReceivedMessageResult = {
|
|
24
|
+
ack: boolean;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Apply a single hub-received message for a group.
|
|
28
|
+
*
|
|
29
|
+
* Returns `{ ack: true }` when the message has been processed to a state where
|
|
30
|
+
* redelivery serves no purpose (applied successfully, or decrypt failed with
|
|
31
|
+
* no recovery path). Returns `{ ack: false }` when the hub should redeliver
|
|
32
|
+
* (transient apply or save failure, MLS state missing, unexpected error).
|
|
33
|
+
* Skips with `{ ack: false }` on groupID mismatch so the message stays queued
|
|
34
|
+
* for whichever recipient it was intended for.
|
|
35
|
+
*/
|
|
36
|
+
export declare function handleReceivedMessage(params: HandleReceivedMessageParams): Promise<HandleReceivedMessageResult>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{fromB64 as e}from"@enkaku/codec";import{ApplyError as r,DecryptError as a}from"../groups/broadcast-service.js";import{fromMLSStateRow as s,toMLSStateInsert as t}from"../groups/mls-state.js";export async function handleReceivedMessage(c){let n,{groupID:u,deviceID:i,message:o,p2pStore:p,broadcastService:l,logger:g}=c;if(o.groupID!==u)return g?.debug("hub message groupID mismatch, skipping",{groupID:u,messageGroupID:o.groupID,sequenceID:o.sequenceID}),{ack:!1};let d=await p.getMLSState(u,i);if(null==d)return g?.warn("mls state missing for group, skipping message",{groupID:u,sequenceID:o.sequenceID}),{ack:!1};let m=s(d),D=e(o.payload);try{n=await l.processReceived({groupID:u,encrypted:D,groupState:m})}catch(e){if(e instanceof a)return g?.debug("decrypt failed, acking and skipping",{groupID:u,sequenceID:o.sequenceID,error:e}),{ack:!0};if(e instanceof r)return g?.error("broadcast apply failed, will redeliver",{groupID:u,sequenceID:o.sequenceID,error:e}),{ack:!1};return g?.error("unexpected error processing hub message",{groupID:u,sequenceID:o.sequenceID,error:e}),{ack:!1}}try{await p.saveMLSState(t(n.updatedGroupState,u,i))}catch(e){return g?.error("save mls state failed after apply, will redeliver",{groupID:u,sequenceID:o.sequenceID,error:e}),{ack:!1}}return{ack:!0}}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Client } from '@enkaku/client';
|
|
2
|
+
import type { HubProtocol } from '@enkaku/hub-protocol';
|
|
3
|
+
import type { Logger } from '@kubun/logger';
|
|
4
|
+
import type { P2PStoreAPI } from '@kubun/store-p2p';
|
|
5
|
+
import type { GroupBroadcastMessage } from '../groups/broadcast.js';
|
|
6
|
+
import type { BroadcastService } from '../groups/broadcast-service.js';
|
|
7
|
+
import { GroupChannel, type GroupChannelParams } from './group-channel.js';
|
|
8
|
+
export type CreateHubClient = (hubURL: string) => Promise<Client<HubProtocol>>;
|
|
9
|
+
export type CreateGroupChannel = (params: GroupChannelParams) => GroupChannel;
|
|
10
|
+
export type HubRelayManagerParams = {
|
|
11
|
+
p2pStore: P2PStoreAPI;
|
|
12
|
+
broadcastService: BroadcastService;
|
|
13
|
+
deviceID: string;
|
|
14
|
+
createHubClient: CreateHubClient;
|
|
15
|
+
logger?: Logger;
|
|
16
|
+
ackFlushMs?: number;
|
|
17
|
+
ackFlushMax?: number;
|
|
18
|
+
backoffBaseMs?: number;
|
|
19
|
+
backoffMaxMs?: number;
|
|
20
|
+
backoffJitter?: number;
|
|
21
|
+
createGroupChannel?: CreateGroupChannel;
|
|
22
|
+
};
|
|
23
|
+
export declare class HubRelayManager {
|
|
24
|
+
#private;
|
|
25
|
+
constructor(params: HubRelayManagerParams);
|
|
26
|
+
/**
|
|
27
|
+
* Open channels for each group in parallel. Per-group failures are logged
|
|
28
|
+
* and swallowed: this method never rejects, so a single bad group does not
|
|
29
|
+
* block the others from starting. Callers who need per-group readiness
|
|
30
|
+
* signals must check `addGroup` / `broadcast` results individually.
|
|
31
|
+
*/
|
|
32
|
+
start(groupIDs: Array<string>): Promise<void>;
|
|
33
|
+
addGroup(groupID: string): Promise<void>;
|
|
34
|
+
removeGroup(groupID: string): Promise<void>;
|
|
35
|
+
broadcast(groupID: string, message: GroupBroadcastMessage): Promise<void>;
|
|
36
|
+
stop(): Promise<void>;
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{HubConnectionPool as e}from"./connection-pool.js";import{GroupChannel as a}from"./group-channel.js";export class HubRelayManager{#e;#a;#t;#s;#r;#o;#i;#l;#c;#h;#u;#p=new Map;constructor(t){this.#e=t.p2pStore,this.#a=t.broadcastService,this.#t=t.deviceID,this.#r=t.logger,this.#o=t.ackFlushMs??500,this.#i=t.ackFlushMax??10,this.#l=t.backoffBaseMs??1e3,this.#c=t.backoffMaxMs??6e4,this.#h=t.backoffJitter??.25,this.#s=new e({createHubClient:t.createHubClient,logger:t.logger}),this.#u=t.createGroupChannel??(e=>new a(e))}async start(e){(await Promise.allSettled(e.map(e=>this.addGroup(e)))).forEach((a,t)=>{"rejected"===a.status&&this.#r?.error("addGroup failed during start",{groupID:e[t],error:a.reason})})}async addGroup(e){if(this.#p.has(e))return void this.#r?.debug("group already added",{groupID:e});let a=await this.#e.getGroup(e);if(null==a)throw Error(`Group ${e} not found`);let t=a.hub_urls;if(null==t||0===t.length)return void this.#r?.warn("group has no hub URLs, skipping",{groupID:e});t.length>1&&this.#r?.warn("multi-hub not yet supported, using first",{groupID:e,hubURLs:t});let s=this.#u({groupID:e,hubURL:t[0],deviceID:this.#t,pool:this.#s,broadcastService:this.#a,p2pStore:this.#e,logger:this.#r,ackFlushMs:this.#o,ackFlushMax:this.#i,backoffBaseMs:this.#l,backoffMaxMs:this.#c,backoffJitter:this.#h});await s.open(),this.#p.set(e,s)}async removeGroup(e){let a=this.#p.get(e);null!=a&&(this.#p.delete(e),await a.close())}async broadcast(e,a){let t=this.#p.get(e);if(null==t)throw Error(`Group ${e} is not active in HubRelayManager`);await t.broadcast(a)}async stop(){let e=[...this.#p.values()];this.#p.clear(),(await Promise.allSettled(e.map(e=>e.close()))).forEach(e=>{"rejected"===e.status&&this.#r?.warn("GroupChannel close failed",{error:e.reason})}),await this.#s.disposeAll()}}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Client } from '@enkaku/client';
|
|
2
|
+
import type { HubProtocol } from '@enkaku/hub-protocol';
|
|
3
|
+
import type { Logger } from '@kubun/logger';
|
|
4
|
+
import type { P2PStoreAPI } from '@kubun/store-p2p';
|
|
5
|
+
import type { GroupBroadcastMessage } from '../groups/broadcast.js';
|
|
6
|
+
import type { BroadcastService } from '../groups/broadcast-service.js';
|
|
7
|
+
export type HandleSendBroadcastParams = {
|
|
8
|
+
groupID: string;
|
|
9
|
+
deviceID: string;
|
|
10
|
+
message: GroupBroadcastMessage;
|
|
11
|
+
p2pStore: P2PStoreAPI;
|
|
12
|
+
broadcastService: BroadcastService;
|
|
13
|
+
hubClient: Client<HubProtocol>;
|
|
14
|
+
logger?: Logger;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Encrypt + send a broadcast for a group, then persist the advanced MLS state.
|
|
18
|
+
*
|
|
19
|
+
* Failure semantics:
|
|
20
|
+
* - Missing MLS state for (group, device) → throw, caller cannot broadcast.
|
|
21
|
+
* - `prepareSend` failure → throw, no save (epoch unchanged on disk).
|
|
22
|
+
* - `hub/group/send` failure → throw, do NOT save advanced MLS state.
|
|
23
|
+
* Prevents an MLS epoch wedge where local state outpaces recipients.
|
|
24
|
+
* - `saveMLSState` failure → throw. State has advanced in memory; on next call
|
|
25
|
+
* the load-from-disk path will re-derive a fresh handle from the prior epoch
|
|
26
|
+
* and re-encrypt. (Wedge protection only applies pre-save by design.)
|
|
27
|
+
*/
|
|
28
|
+
export declare function handleSendBroadcast(params: HandleSendBroadcastParams): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{toB64 as a}from"@enkaku/codec";import{fromMLSStateRow as r,toMLSStateInsert as t}from"../groups/mls-state.js";export async function handleSendBroadcast(e){let{groupID:o,deviceID:s,message:n,p2pStore:i,broadcastService:p,hubClient:d,logger:u}=e,c=await i.getMLSState(o,s);if(null==c)throw Error(`MLS state missing for group ${o}; cannot broadcast`);let l=r(c),{encrypted:m,updatedGroupState:S}=await p.prepareSend({groupID:o,message:n,groupState:l});try{await d.request("hub/group/send",{param:{groupID:o,payload:a(m)}})}catch(a){throw u?.error("hub/group/send failed",{groupID:o,error:a}),a}await i.saveMLSState(t(S,o,s))}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import type { KubunPlugin, PluginFactoryParams } from '@kubun/engine';
|
|
2
2
|
export type { ContextDeps, PendingJoinRequest } from './context/types.js';
|
|
3
|
-
export {
|
|
3
|
+
export { type GroupBroadcastMessage, type ProcessBroadcastParams, processBroadcast, } from './groups/broadcast.js';
|
|
4
|
+
export { deserializeBroadcast, serializeBroadcast } from './groups/broadcast-codec.js';
|
|
5
|
+
export { ApplyError, type BroadcastEvent, BroadcastService, type BroadcastServiceEvents, type BroadcastServiceParams, DecryptError, type ReceiveBroadcastParams, type ReceiveBroadcastResult, type SendBroadcastParams, type SendBroadcastResult, } from './groups/broadcast-service.js';
|
|
6
|
+
export { createFilteredGenerator, createGroupEventEmitter, type GroupEventEmitter, type GroupEventMap, } from './groups/events.js';
|
|
7
|
+
export { decodeFullJoinRequest, decodeInvitePayload, decodeJoinRequest, encodeFullJoinRequest, encodeInvitePayload, encodeJoinRequest, type FullJoinRequestPayload, type InvitePayload, type JoinRequestPayload, } from './groups/invite-payload.js';
|
|
8
|
+
export { type AddCircleMemberParams, type AddCircleMemberResult, type CreateCircleParams, type CreateCircleResult, type CreateGroupParams, type CreateGroupResult, type DeleteCircleParams, type DeleteCircleResult, GroupManager, type GroupManagerParams, type InviteToGroupParams, type InviteToGroupResult, type JoinGroupParams, type JoinGroupResult, type LeaveGroupParams, type LeaveGroupResult, type RemoveCircleMemberParams, type RemoveCircleMemberResult, type RemoveGroupMemberParams, type RemoveGroupMemberResult, type RemoveMemberParams, type RemoveMemberResult, type UpdateCircleParams, type UpdateCircleResult, type UpdateGroupParams, type UpdateGroupResult, } from './groups/manager.js';
|
|
9
|
+
export { type MLSGroupHandle, restoreMLSGroupHandle } from './groups/mls-group-handle.js';
|
|
10
|
+
export { replacer as mlsJSONReplacer, reviver as mlsJSONReviver } from './groups/mls-json.js';
|
|
11
|
+
export { deserializeMLSGroupState, type MLSGroupState, type SerializedMLSGroupState, serializeMLSGroupState, } from './groups/mls-state.js';
|
|
4
12
|
export type { SyncClientMessage, SyncProtocol, SyncServerMessage } from './protocol.js';
|
|
5
13
|
export { createP2PSchemaExtension } from './schema.js';
|
|
6
|
-
export { type ProcessBroadcastParams, processBroadcast, type SpaceBroadcastMessage, } from './spaces/broadcast.js';
|
|
7
|
-
export { deserializeBroadcast, serializeBroadcast } from './spaces/broadcast-codec.js';
|
|
8
|
-
export { type BroadcastEvent, BroadcastService, type BroadcastServiceEvents, type BroadcastServiceParams, type ReceiveBroadcastParams, type ReceiveBroadcastResult, type SendBroadcastParams, type SendBroadcastResult, } from './spaces/broadcast-service.js';
|
|
9
|
-
export { createFilteredGenerator, createSpaceEventEmitter, type SpaceEventEmitter, type SpaceEventMap, } from './spaces/events.js';
|
|
10
|
-
export { restoreGroupHandle } from './spaces/group-handle.js';
|
|
11
|
-
export { decodeFullJoinRequest, decodeInvitePayload, decodeJoinRequest, encodeFullJoinRequest, encodeInvitePayload, encodeJoinRequest, type FullJoinRequestPayload, type InvitePayload, type JoinRequestPayload, } from './spaces/invite-payload.js';
|
|
12
|
-
export { type AddCircleMemberParams, type AddCircleMemberResult, type CreateCircleParams, type CreateCircleResult, type CreateSpaceParams, type CreateSpaceResult, type DeleteCircleParams, type DeleteCircleResult, type InviteToSpaceParams, type InviteToSpaceResult, type JoinSpaceParams, type JoinSpaceResult, type LeaveSpaceParams, type LeaveSpaceResult, type QueryRunner, type RemoveCircleMemberParams, type RemoveCircleMemberResult, type RemoveMemberParams, type RemoveMemberResult, type RemoveSpaceMemberParams, type RemoveSpaceMemberResult, SpaceManager, type SpaceManagerParams, type UpdateCircleParams, type UpdateCircleResult, type UpdateSpaceParams, type UpdateSpaceResult, } from './spaces/manager.js';
|
|
13
|
-
export { replacer as mlsJSONReplacer, reviver as mlsJSONReviver } from './spaces/mls-json.js';
|
|
14
|
-
export { deserializeGroupState, type GroupState, type SerializedGroupState, serializeGroupState, } from './spaces/mls-state.js';
|
|
15
14
|
export { type CatalogSyncScope, resolveCatalogSyncScopes } from './sync/catalog-scope.js';
|
|
16
|
-
export { checkSyncDelegation } from './sync/handlers.js';
|
|
15
|
+
export { checkSyncDelegation, negotiateDirection } from './sync/handlers.js';
|
|
17
16
|
export { type PeerConfig, type PeerConfigWithID, PeerRegistry } from './sync/peer-registry.js';
|
|
18
|
-
export {
|
|
17
|
+
export { SyncClient, type SyncClientParams, type SyncScope, type SyncTransportProvider, } from './sync/sync-client.js';
|
|
19
18
|
export { type SyncEvent, SyncManager, type SyncManagerParams, type SyncSessionInfo, type SyncStatus, } from './sync/sync-manager.js';
|
|
20
|
-
export type {
|
|
19
|
+
export type { GroupChannel, GroupChannelParams } from './hub/group-channel.js';
|
|
20
|
+
export { type CreateGroupChannel, type CreateHubClient, HubRelayManager, type HubRelayManagerParams, } from './hub/relay-manager.js';
|
|
21
|
+
export type { CatalogData, CircleData, CircleMemberData, DeleteCatalogData, DeleteCircleData, GroupData, GroupMemberData, GroupRequestContext, InviteToGroupData, JoinGroupData, LeaveGroupData, P2PJoinRequestContext, RemoveCircleMemberData, RemoveGroupMemberData, SyncPluginAPI, SyncRequestContext, SyncSessionResult, UpdateCircleInput, UpdateGroupInput, } from './types.js';
|
|
21
22
|
export type P2PPluginOptions = {
|
|
22
23
|
autoAcceptPeers?: Array<string>;
|
|
23
24
|
http?: boolean | string;
|
package/lib/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ServerTransport as e}from"@enkaku/http-server-transport";import{Server as
|
|
1
|
+
import{ServerTransport as e}from"@enkaku/http-server-transport";import{Server as r}from"@enkaku/server";import{DirectTransports as t}from"@enkaku/transport";import{HLC as o}from"@kubun/hlc";import{p2pStoreDefinition as s}from"@kubun/store-p2p";import{createGroupContext as n}from"./context/group.js";import{createJoinContext as a}from"./context/join.js";import{createSyncContext as i}from"./context/sync.js";import{createGroupEventEmitter as p}from"./groups/events.js";import{GroupManager as c}from"./groups/manager.js";import{createP2PSchemaExtension as m}from"./schema.js";import{createSyncHandlers as g}from"./sync/handlers.js";import{SyncManager as d}from"./sync/sync-manager.js";export{processBroadcast}from"./groups/broadcast.js";export{deserializeBroadcast,serializeBroadcast}from"./groups/broadcast-codec.js";export{ApplyError,BroadcastService,DecryptError}from"./groups/broadcast-service.js";export{createFilteredGenerator,createGroupEventEmitter}from"./groups/events.js";export{decodeFullJoinRequest,decodeInvitePayload,decodeJoinRequest,encodeFullJoinRequest,encodeInvitePayload,encodeJoinRequest}from"./groups/invite-payload.js";export{GroupManager}from"./groups/manager.js";export{restoreMLSGroupHandle}from"./groups/mls-group-handle.js";export{replacer as mlsJSONReplacer,reviver as mlsJSONReviver}from"./groups/mls-json.js";export{deserializeMLSGroupState,serializeMLSGroupState}from"./groups/mls-state.js";export{createP2PSchemaExtension}from"./schema.js";export{resolveCatalogSyncScopes}from"./sync/catalog-scope.js";export{checkSyncDelegation,negotiateDirection}from"./sync/handlers.js";export{PeerRegistry}from"./sync/peer-registry.js";export{SyncClient}from"./sync/sync-client.js";export{SyncManager}from"./sync/sync-manager.js";export{HubRelayManager}from"./hub/relay-manager.js";export function createP2PPlugin(l){return u=>{let y,f;u.db.register(s);let j=u.db.adapter,h=u.identity,v=new o({nodeID:h.id}),x=new c({identity:h,getRandomID:u.runtime.getRandomID}),P=p(),S=new d({deployClusters:async e=>{let r=Object.values(e);await u.engine.deployGraph({clusters:r})},runtime:u.runtime,identity:u.identity,logger:u.getLogger("sync")}),b=g({db:u.db,logger:u.getLogger("sync-handlers"),peerRegistry:S.peerRegistry}),R=new Map,w=[],k={addPeer:e=>S.addPeer({config:e,stores:u.db}),removePeer:e=>S.removePeer({peerDID:e,stores:u.db}),listPeers:()=>S.listPeers({stores:u.db}),syncWithPeer:e=>S.merkleSyncWithPeer({...e,stores:u.db}),getStatus:e=>S.getStatus({peerDID:e,stores:u.db}),onSyncEvent:e=>S.onSyncEvent(e),createSyncTransport:function(e){let o=new t({signal:e}),s=new r({accessControl:!1,getRandomID:u.runtime.getRandomID,handlers:b,logger:u.getLogger("sync-server"),transports:[o.server],signal:e});return w.push(s),o.client}};if(l?.http){let t="string"==typeof l.http?l.http.replace(/^\//,""):"sync";k.syncReady=(async()=>{let o=await u.engine.getAPI("http");await o.listening,y=new e({allowedOrigin:["*"]}),f=new r({accessControl:!1,getRandomID:u.runtime.getRandomID,handlers:b,logger:u.getLogger("sync-http-server"),transports:[y]}),w.push(f),o.registerProtocol(t,y.fetch.bind(y))})()}return{name:"p2p",schemaExtension:e=>m(P),api:k,createContextFactory:()=>(e,r)=>{let t={identity:h,groupManager:x,syncManager:S,stores:r,adapter:j,hlc:v,emitter:P,pendingJoinRequests:R,runtime:u.runtime,autoAcceptPeers:l?.autoAcceptPeers};return{...n(e,t),...i(e,t),...a(e,t)}},dispose:async()=>{for(let e of w)await e.dispose();await y?.dispose(),await S.dispose()}}}}
|
package/lib/protocol.d.ts
CHANGED
|
@@ -44,6 +44,10 @@ export declare const syncProtocol: {
|
|
|
44
44
|
};
|
|
45
45
|
readonly description: "Model IDs the requester already has — used to identify missing clusters";
|
|
46
46
|
};
|
|
47
|
+
readonly direction: {
|
|
48
|
+
readonly type: "string";
|
|
49
|
+
readonly enum: readonly ["push", "pull", "both"];
|
|
50
|
+
};
|
|
47
51
|
};
|
|
48
52
|
readonly required: readonly ["delegationTokens"];
|
|
49
53
|
readonly additionalProperties: false;
|
|
@@ -51,6 +55,10 @@ export declare const syncProtocol: {
|
|
|
51
55
|
readonly result: {
|
|
52
56
|
readonly type: "object";
|
|
53
57
|
readonly properties: {
|
|
58
|
+
readonly direction: {
|
|
59
|
+
readonly type: "string";
|
|
60
|
+
readonly enum: readonly ["push", "pull", "both"];
|
|
61
|
+
};
|
|
54
62
|
readonly acceptedScopes: {
|
|
55
63
|
readonly type: "array";
|
|
56
64
|
readonly items: {
|
|
@@ -84,7 +92,7 @@ export declare const syncProtocol: {
|
|
|
84
92
|
};
|
|
85
93
|
};
|
|
86
94
|
readonly 'sync/merkle-sync': {
|
|
87
|
-
readonly type: "
|
|
95
|
+
readonly type: "channel";
|
|
88
96
|
readonly param: {
|
|
89
97
|
readonly type: "object";
|
|
90
98
|
readonly properties: {
|
|
@@ -117,11 +125,15 @@ export declare const syncProtocol: {
|
|
|
117
125
|
readonly type: "string";
|
|
118
126
|
};
|
|
119
127
|
};
|
|
128
|
+
readonly direction: {
|
|
129
|
+
readonly type: "string";
|
|
130
|
+
readonly enum: readonly ["push", "pull", "both"];
|
|
131
|
+
};
|
|
120
132
|
};
|
|
121
|
-
readonly required: readonly ["scopes", "excludedDocumentIDs", "tree"];
|
|
133
|
+
readonly required: readonly ["scopes", "excludedDocumentIDs", "tree", "direction"];
|
|
122
134
|
readonly additionalProperties: false;
|
|
123
135
|
};
|
|
124
|
-
readonly
|
|
136
|
+
readonly send: {
|
|
125
137
|
readonly type: "object";
|
|
126
138
|
readonly properties: {
|
|
127
139
|
readonly type: {
|
|
@@ -135,13 +147,30 @@ export declare const syncProtocol: {
|
|
|
135
147
|
};
|
|
136
148
|
readonly description: "Signed mutation JWTs for divergent time buckets";
|
|
137
149
|
};
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
150
|
+
};
|
|
151
|
+
readonly required: readonly ["type"];
|
|
152
|
+
readonly additionalProperties: false;
|
|
153
|
+
};
|
|
154
|
+
readonly receive: {
|
|
155
|
+
readonly type: "object";
|
|
156
|
+
readonly properties: {
|
|
157
|
+
readonly type: {
|
|
158
|
+
readonly type: "string";
|
|
159
|
+
readonly enum: readonly ["tree", "mutations", "complete"];
|
|
141
160
|
};
|
|
142
|
-
readonly
|
|
143
|
-
readonly type: "
|
|
144
|
-
readonly description: "
|
|
161
|
+
readonly tree: {
|
|
162
|
+
readonly type: "object";
|
|
163
|
+
readonly description: "Responder merkle tree bucket hashes";
|
|
164
|
+
readonly additionalProperties: {
|
|
165
|
+
readonly type: "string";
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
readonly mutationJWTs: {
|
|
169
|
+
readonly type: "array";
|
|
170
|
+
readonly items: {
|
|
171
|
+
readonly type: "string";
|
|
172
|
+
};
|
|
173
|
+
readonly description: "Signed mutation JWTs for divergent time buckets";
|
|
145
174
|
};
|
|
146
175
|
};
|
|
147
176
|
readonly required: readonly ["type"];
|
|
@@ -159,8 +188,11 @@ export declare const syncProtocol: {
|
|
|
159
188
|
readonly mutationsSent: {
|
|
160
189
|
readonly type: "number";
|
|
161
190
|
};
|
|
191
|
+
readonly mutationsReceived: {
|
|
192
|
+
readonly type: "number";
|
|
193
|
+
};
|
|
162
194
|
};
|
|
163
|
-
readonly required: readonly ["success", "divergentBuckets", "mutationsSent"];
|
|
195
|
+
readonly required: readonly ["success", "divergentBuckets", "mutationsSent", "mutationsReceived"];
|
|
164
196
|
readonly additionalProperties: false;
|
|
165
197
|
};
|
|
166
198
|
};
|
|
@@ -169,7 +201,9 @@ export type SyncProtocol = typeof syncProtocol;
|
|
|
169
201
|
export type SyncNegotiateParams = FromSchema<SyncProtocol['sync/negotiate']['param']>;
|
|
170
202
|
export type SyncNegotiateResult = FromSchema<SyncProtocol['sync/negotiate']['result']>;
|
|
171
203
|
export type SyncMerkleSyncParams = FromSchema<SyncProtocol['sync/merkle-sync']['param']>;
|
|
204
|
+
export type SyncMerkleSyncSend = FromSchema<SyncProtocol['sync/merkle-sync']['send']>;
|
|
172
205
|
export type SyncMerkleSyncReceive = FromSchema<SyncProtocol['sync/merkle-sync']['receive']>;
|
|
173
206
|
export type SyncMerkleSyncResult = FromSchema<SyncProtocol['sync/merkle-sync']['result']>;
|
|
207
|
+
export type SyncDirection = 'push' | 'pull' | 'both';
|
|
174
208
|
export type SyncClientMessage = AnyClientMessageOf<SyncProtocol>;
|
|
175
209
|
export type SyncServerMessage = AnyServerMessageOf<SyncProtocol>;
|
package/lib/protocol.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const syncProtocol={"sync/negotiate":{type:"request",param:{type:"object",properties:{scopes:{type:"array",items:{type:"object",properties:{modelID:{type:"string"},ownerDID:{type:"string"}},required:["modelID","ownerDID"],additionalProperties:!1},description:"Requested sync scopes as (model, owner) pairs"},delegationTokens:{type:"array",items:{type:"string"},description:"JWT delegation tokens for authorization"},catalogIDs:{type:"array",items:{type:"string"},description:"Catalog IDs to resolve sync scope from criteria"},knownModelIDs:{type:"array",items:{type:"string"},description:"Model IDs the requester already has — used to identify missing clusters"}},required:["delegationTokens"],additionalProperties:!1},result:{type:"object",properties:{acceptedScopes:{type:"array",items:{type:"object",properties:{modelID:{type:"string"},ownerDID:{type:"string"}},required:["modelID","ownerDID"],additionalProperties:!1}},excludedDocumentIDs:{type:"array",items:{type:"string"}},missingClusters:{type:"object",description:"Cluster definitions for models the requester does not have yet",additionalProperties:!0}},required:["acceptedScopes","excludedDocumentIDs"],additionalProperties:!1}},"sync/merkle-sync":{type:"
|
|
1
|
+
export const syncProtocol={"sync/negotiate":{type:"request",param:{type:"object",properties:{scopes:{type:"array",items:{type:"object",properties:{modelID:{type:"string"},ownerDID:{type:"string"}},required:["modelID","ownerDID"],additionalProperties:!1},description:"Requested sync scopes as (model, owner) pairs"},delegationTokens:{type:"array",items:{type:"string"},description:"JWT delegation tokens for authorization"},catalogIDs:{type:"array",items:{type:"string"},description:"Catalog IDs to resolve sync scope from criteria"},knownModelIDs:{type:"array",items:{type:"string"},description:"Model IDs the requester already has — used to identify missing clusters"},direction:{type:"string",enum:["push","pull","both"]}},required:["delegationTokens"],additionalProperties:!1},result:{type:"object",properties:{direction:{type:"string",enum:["push","pull","both"]},acceptedScopes:{type:"array",items:{type:"object",properties:{modelID:{type:"string"},ownerDID:{type:"string"}},required:["modelID","ownerDID"],additionalProperties:!1}},excludedDocumentIDs:{type:"array",items:{type:"string"}},missingClusters:{type:"object",description:"Cluster definitions for models the requester does not have yet",additionalProperties:!0}},required:["acceptedScopes","excludedDocumentIDs"],additionalProperties:!1}},"sync/merkle-sync":{type:"channel",param:{type:"object",properties:{scopes:{type:"array",items:{type:"object",properties:{modelID:{type:"string"},ownerDID:{type:"string"}},required:["modelID","ownerDID"],additionalProperties:!1}},excludedDocumentIDs:{type:"array",items:{type:"string"}},tree:{type:"object",description:"Merkle tree bucket hashes from requester",additionalProperties:{type:"string"}},direction:{type:"string",enum:["push","pull","both"]}},required:["scopes","excludedDocumentIDs","tree","direction"],additionalProperties:!1},send:{type:"object",properties:{type:{type:"string",enum:["mutations","complete"]},mutationJWTs:{type:"array",items:{type:"string"},description:"Signed mutation JWTs for divergent time buckets"}},required:["type"],additionalProperties:!1},receive:{type:"object",properties:{type:{type:"string",enum:["tree","mutations","complete"]},tree:{type:"object",description:"Responder merkle tree bucket hashes",additionalProperties:{type:"string"}},mutationJWTs:{type:"array",items:{type:"string"},description:"Signed mutation JWTs for divergent time buckets"}},required:["type"],additionalProperties:!1},result:{type:"object",properties:{success:{type:"boolean"},divergentBuckets:{type:"number"},mutationsSent:{type:"number"},mutationsReceived:{type:"number"}},required:["success","divergentBuckets","mutationsSent","mutationsReceived"],additionalProperties:!1}}};
|
package/lib/schema.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { SchemaExtension } from '@kubun/engine';
|
|
2
|
-
import { type
|
|
3
|
-
export declare function createP2PSchemaExtension(emitter:
|
|
2
|
+
import { type GroupEventEmitter } from './groups/events.js';
|
|
3
|
+
export declare function createP2PSchemaExtension(emitter: GroupEventEmitter): SchemaExtension;
|
package/lib/schema.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{createFilteredGenerator as e}from"./
|
|
2
|
-
type
|
|
1
|
+
import{createFilteredGenerator as e}from"./groups/events.js";let r=`
|
|
2
|
+
type PeerGroup {
|
|
3
3
|
id: ID!
|
|
4
4
|
name: String!
|
|
5
5
|
description: String
|
|
@@ -7,8 +7,8 @@ type PeerSpace {
|
|
|
7
7
|
createdAt: String!
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
type
|
|
11
|
-
|
|
10
|
+
type PeerGroupMember {
|
|
11
|
+
groupID: ID!
|
|
12
12
|
memberDID: String!
|
|
13
13
|
role: String!
|
|
14
14
|
createdAt: String!
|
|
@@ -16,7 +16,7 @@ type PeerSpaceMember {
|
|
|
16
16
|
|
|
17
17
|
type PeerCircle {
|
|
18
18
|
id: ID!
|
|
19
|
-
|
|
19
|
+
groupID: ID!
|
|
20
20
|
name: String!
|
|
21
21
|
description: String
|
|
22
22
|
createdAt: String!
|
|
@@ -38,23 +38,23 @@ type PeerCatalog {
|
|
|
38
38
|
createdAt: String!
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
input
|
|
41
|
+
input CreateGroupInput {
|
|
42
42
|
name: String!
|
|
43
43
|
description: String
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
type
|
|
47
|
-
|
|
46
|
+
type CreateGroupPayload {
|
|
47
|
+
group: PeerGroup!
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
type
|
|
50
|
+
type InviteToGroupPayload {
|
|
51
51
|
invitePayload: String!
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
groupID: ID!
|
|
53
|
+
groupName: String!
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
type
|
|
57
|
-
|
|
56
|
+
type JoinGroupPayload {
|
|
57
|
+
group: PeerGroup
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
type PrepareJoinRequestPayload {
|
|
@@ -62,15 +62,15 @@ type PrepareJoinRequestPayload {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
type CompleteJoinPayload {
|
|
65
|
-
|
|
65
|
+
group: PeerGroup
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
type
|
|
69
|
-
|
|
68
|
+
type LeaveGroupPayload {
|
|
69
|
+
groupID: ID!
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
type
|
|
73
|
-
|
|
72
|
+
type RemoveGroupMemberPayload {
|
|
73
|
+
groupID: ID!
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
input UpdateCatalogInput {
|
|
@@ -79,14 +79,14 @@ input UpdateCatalogInput {
|
|
|
79
79
|
filterCriteria: String
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
input
|
|
83
|
-
|
|
82
|
+
input UpdateGroupInput {
|
|
83
|
+
groupID: ID!
|
|
84
84
|
name: String
|
|
85
85
|
description: String
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
type
|
|
89
|
-
|
|
88
|
+
type UpdateGroupPayload {
|
|
89
|
+
group: PeerGroup!
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
input UpdateCircleInput {
|
|
@@ -141,28 +141,28 @@ type RemoveSyncPeerPayload {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
extend type Query {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
circles(
|
|
144
|
+
groups: [PeerGroup!]!
|
|
145
|
+
group(id: ID!): PeerGroup
|
|
146
|
+
groupMembers(groupID: ID!): [PeerGroupMember!]!
|
|
147
|
+
circles(groupID: ID!): [PeerCircle!]!
|
|
148
148
|
syncPeers: [SyncPeer!]!
|
|
149
149
|
syncStatus: SyncStatus!
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
extend type Mutation {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
createCircle(
|
|
153
|
+
createGroup(input: CreateGroupInput!): CreateGroupPayload!
|
|
154
|
+
addGroupMember(groupID: ID!, memberDID: String!, role: String!): PeerGroupMember!
|
|
155
|
+
createCircle(groupID: ID!, name: String!, description: String): PeerCircle!
|
|
156
156
|
addCircleMember(circleID: ID!, memberDID: String!, role: String!): PeerCircleMember!
|
|
157
157
|
createCatalog(name: String!, description: String, filterCriteria: String!): PeerCatalog!
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
inviteToGroup(groupID: ID!, joinRequest: String!): InviteToGroupPayload!
|
|
159
|
+
joinGroup(invitePayload: String!, joinRequestPayload: String!): JoinGroupPayload!
|
|
160
160
|
prepareJoinRequest: PrepareJoinRequestPayload!
|
|
161
161
|
completeJoin(invitePayload: String!): CompleteJoinPayload!
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
leaveGroup(groupID: ID!): LeaveGroupPayload!
|
|
163
|
+
removeGroupMember(groupID: ID!, memberDID: String!): RemoveGroupMemberPayload!
|
|
164
164
|
updateCatalog(input: UpdateCatalogInput!): PeerCatalog!
|
|
165
|
-
|
|
165
|
+
updateGroup(input: UpdateGroupInput!): UpdateGroupPayload!
|
|
166
166
|
updateCircle(input: UpdateCircleInput!): UpdateCirclePayload!
|
|
167
167
|
deleteCatalog(catalogID: ID!): DeleteCatalogPayload!
|
|
168
168
|
deleteCircle(circleID: ID!): DeleteCirclePayload!
|
|
@@ -172,16 +172,16 @@ extend type Mutation {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
extend type Subscription {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
circleCreated(
|
|
181
|
-
circleDeleted(
|
|
175
|
+
groupJoined: PeerGroup!
|
|
176
|
+
groupLeft: PeerGroup!
|
|
177
|
+
groupDataChanged(groupID: ID!): PeerGroup!
|
|
178
|
+
groupMemberJoined(groupID: ID!): PeerGroupMember!
|
|
179
|
+
groupMemberLeft(groupID: ID!): PeerGroupMember!
|
|
180
|
+
circleCreated(groupID: ID!): PeerCircle!
|
|
181
|
+
circleDeleted(groupID: ID!): PeerCircle!
|
|
182
182
|
circleDataChanged(circleID: ID!): PeerCircle!
|
|
183
183
|
circleMemberAdded(circleID: ID!): PeerCircleMember!
|
|
184
184
|
circleMemberRemoved(circleID: ID!): PeerCircleMember!
|
|
185
185
|
circleCatalogsChanged(circleID: ID!): PeerCircle!
|
|
186
186
|
}
|
|
187
|
-
`;export function createP2PSchemaExtension(
|
|
187
|
+
`;export function createP2PSchemaExtension(t){return{sdl:r,resolvers:{queryFields:{groups:async(e,r,t)=>await t.p2pGetGroups(),group:async(e,r,t)=>await t.p2pGetGroup(r.id),groupMembers:async(e,r,t)=>await t.p2pGetGroupMembers(r.groupID),circles:async(e,r,t)=>await t.p2pGetCircles(r.groupID),syncPeers:async(e,r,t)=>await t.syncListPeers(),syncStatus:(e,r,t)=>{let i=t.syncGetStatus();return{...i,lastSyncByPeer:JSON.stringify(i.lastSyncByPeer)}}},mutationFields:{createGroup:async(e,r,t)=>{let i=r.input;return{group:await t.p2pCreateGroup(i)}},addGroupMember:async(e,r,t)=>await t.p2pAddGroupMember(r.groupID,r.memberDID,r.role),createCircle:async(e,r,t)=>await t.p2pCreateCircle({groupID:r.groupID,name:r.name,description:r.description}),addCircleMember:async(e,r,t)=>await t.p2pAddCircleMember(r.circleID,r.memberDID,r.role),createCatalog:async(e,r,t)=>await t.p2pCreateCatalog({name:r.name,description:r.description,filterCriteria:r.filterCriteria}),inviteToGroup:async(e,r,t)=>await t.p2pInviteToGroup(r.groupID,r.joinRequest),joinGroup:async(e,r,t)=>await t.p2pJoinGroup(r.invitePayload,r.joinRequestPayload),prepareJoinRequest:async(e,r,t)=>await t.p2pPrepareJoinRequest(),completeJoin:async(e,r,t)=>await t.p2pCompleteJoin(r.invitePayload),leaveGroup:async(e,r,t)=>await t.p2pLeaveGroup(r.groupID),removeGroupMember:async(e,r,t)=>await t.p2pRemoveGroupMember(r.groupID,r.memberDID),updateCatalog:async(e,r,t)=>{let i=r.input;return await t.p2pUpdateCatalog(i)},updateGroup:async(e,r,t)=>{let i=r.input;return{group:await t.p2pUpdateGroup(i)}},updateCircle:async(e,r,t)=>{let i=r.input;return{circle:await t.p2pUpdateCircle(i)}},deleteCatalog:async(e,r,t)=>await t.p2pDeleteCatalog(r.catalogID),deleteCircle:async(e,r,t)=>await t.p2pDeleteCircle(r.circleID),removeCircleMember:async(e,r,t)=>await t.p2pRemoveCircleMember(r.circleID,r.memberDID),addSyncPeer:async(e,r,t)=>(await t.syncAddPeer({peerDID:r.peerDID,endpoint:r.endpoint,mode:r.mode??"on-demand",allowedUsers:{all:!0},priority:r.priority??"0",trustLevel:r.trustLevel??"trusted"}),!0),removeSyncPeer:async(e,r,t)=>(await t.syncRemovePeer(r.peerDID),{success:!0})},subscriptionFields:{groupJoined:{resolve:e=>e,subscribe:()=>e(t,"groupJoined")},groupLeft:{resolve:e=>e,subscribe:()=>e(t,"groupLeft")},groupDataChanged:{resolve:e=>e,subscribe:(r,i)=>e(t,"groupDataChanged",e=>e.groupID===i.groupID)},groupMemberJoined:{resolve:e=>e,subscribe:(r,i)=>e(t,"groupMemberJoined",e=>e.groupID===i.groupID)},groupMemberLeft:{resolve:e=>e,subscribe:(r,i)=>e(t,"groupMemberLeft",e=>e.groupID===i.groupID)},circleCreated:{resolve:e=>e,subscribe:(r,i)=>e(t,"circleCreated",e=>e.groupID===i.groupID)},circleDeleted:{resolve:e=>e,subscribe:(r,i)=>e(t,"circleDeleted",e=>e.groupID===i.groupID)},circleDataChanged:{resolve:e=>e,subscribe:(r,i)=>e(t,"circleDataChanged",e=>e.circleID===i.circleID)},circleMemberAdded:{resolve:e=>e,subscribe:(r,i)=>e(t,"circleMemberAdded",e=>e.circleID===i.circleID)},circleMemberRemoved:{resolve:e=>e,subscribe:(r,i)=>e(t,"circleMemberRemoved",e=>e.circleID===i.circleID)},circleCatalogsChanged:{resolve:e=>e,subscribe:(r,i)=>e(t,"circleCatalogsChanged",e=>e.circleID===i.circleID)}}}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export async function resolveCatalogSyncScopes(
|
|
1
|
+
import{getGraphStore as e}from"@kubun/store-graph";import{getP2PStore as r}from"@kubun/store-p2p";export async function resolveCatalogSyncScopes(l,t,o){let[a,i]=await Promise.all([e(l),r(l)]),s=new Set,f=new Set,n=!1;for(let e of t){let r=await a.resolveCatalogScope(e);if(null!=r.models)for(let e of r.models)s.add(e);if(null!=r.owners)for(let e of(n=!0,r.owners))f.add(e);let l=await a.getCatalog(e);if(null!=l){let e=l.filter_criteria;if(null!=e.circles&&e.circles.length>0)for(let r of(n=!0,e.circles))for(let e of(await i.listCircleMembers(r)))f.add(e.member_did)}}let d=Array.from(s),m=n?Array.from(f):void 0,u=new Set(o??[]),c=new Set,w=new Set;return await Promise.all(d.map(async e=>{let r=await a.getClusterForModel(e);null!=r&&(c.add(r),u.has(e)||w.add(r))})),{modelIDs:d,owners:m,requiredClusterIDs:Array.from(c),missingClusterIDs:Array.from(w)}}
|
package/lib/sync/handlers.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { KubunDB } from '@kubun/db';
|
|
2
2
|
import type { Logger } from '@kubun/logger';
|
|
3
|
+
import type { SyncDirection } from '../protocol.js';
|
|
4
|
+
import type { PeerRegistry } from './peer-registry.js';
|
|
3
5
|
/**
|
|
4
6
|
* Check if delegation tokens grant the viewer read access to a user's documents.
|
|
5
7
|
*
|
|
@@ -12,7 +14,15 @@ export declare function checkSyncDelegation(viewerDID: string, ownerDID: string,
|
|
|
12
14
|
export type CreateSyncHandlersParams = {
|
|
13
15
|
db: KubunDB;
|
|
14
16
|
logger: Logger;
|
|
17
|
+
peerRegistry: PeerRegistry;
|
|
15
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the effective sync direction given what the initiator requested
|
|
21
|
+
* and what the responder's per-peer policy allows.
|
|
22
|
+
* If allowed is 'both' (or unset), the request passes through.
|
|
23
|
+
* Otherwise the responder's policy wins.
|
|
24
|
+
*/
|
|
25
|
+
export declare function negotiateDirection(requested: SyncDirection, allowed: SyncDirection): SyncDirection;
|
|
16
26
|
/**
|
|
17
27
|
* Create sync protocol handlers served by plugin-p2p's own Enkaku server.
|
|
18
28
|
*/
|
package/lib/sync/handlers.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{checkCapability as e}from"@enkaku/capability";import{
|
|
1
|
+
import{checkCapability as e}from"@enkaku/capability";import{getGraphStore as t}from"@kubun/store-graph";import{resolveCatalogSyncScopes as n}from"./catalog-scope.js";import{createMerkleSyncChannelHandler as o}from"./merkle-channel.js";export async function checkSyncDelegation(t,n,o){if(!o||0===o.length)return!1;let r=["*","urn:kubun:user:*",`urn:kubun:user:${n}`],l="document/read";for(let a of r)try{return await e({act:l,res:a},{iss:t,sub:n,cap:o}),!0}catch{}for(let a of o)for(let o of r)try{return await e({act:l,res:o},{iss:t,sub:n,cap:a}),!0}catch{}return!1}export function negotiateDirection(e,t){return"both"===t?e:"both"===e?t:e===t?e:t}export function createSyncHandlers(e){let{db:r,logger:l,peerRegistry:a}=e;return{"sync/negotiate":async e=>{let o,{scopes:s,delegationTokens:i,catalogIDs:c,knownModelIDs:u,direction:g}=e.param,f=e.message.payload,d=f.sub||f.iss;l.debug("sync/negotiate requested",{scopes:s,catalogIDs:c,viewerDID:d});let p=[],D=await t(r);if(null!=c&&c.length>0){let e=await n(r,c,u);if(e.modelIDs.length>0)if(null!=e.owners)for(let t of e.modelIDs)for(let n of e.owners){let e=d===n,o=!e&&await checkSyncDelegation(d,n,i);(e||o)&&p.push({modelID:t,ownerDID:n})}else for(let t of e.modelIDs)for(let e of(await D.getDistinctOwnersForModel(t))){let n=d===e,o=!n&&await checkSyncDelegation(d,e,i);(n||o)&&p.push({modelID:t,ownerDID:e})}e.missingClusterIDs.length>0&&(o=await D.getClusters(e.missingClusterIDs))}if(null!=s)for(let e of s){let t=d===e.ownerDID,n=!t&&await checkSyncDelegation(d,e.ownerDID,i);t||n?p.push(e):l.debug("sync/negotiate: scope rejected",{scope:e,viewerDID:d})}let h=new Set,m=p.filter(e=>{let t=`${e.modelID}:${e.ownerDID}`;return!h.has(t)&&(h.add(t),!0)});l.info("sync/negotiate completed",{requested:(s?.length??0)+(c?.length??0),accepted:m.length,missingClusters:null!=o?Object.keys(o).length:0});let w=g??"pull",y=await a.getPeer({peerDID:d,stores:r}),b=y?.allowedDirection??"both",k=negotiateDirection(w,b);return k!==w&&l.info("sync/negotiate: direction downgraded",{requested:w,allowed:b,agreed:k}),{direction:k,acceptedScopes:m,excludedDocumentIDs:[],...null!=o&&{missingClusters:o}}},"sync/merkle-sync":o({db:r,logger:l})}}
|
package/lib/sync/merkle-apply.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{asType as t,createValidator as a}from"@enkaku/schema";import{verifyToken as i}from"@enkaku/token";import{computeMutationHash as o}from"@kubun/engine";import{DocumentID as n}from"@kubun/id";import{applyMutation as u}from"@kubun/mutation";import{documentMutation as e}from"@kubun/protocol";let
|
|
1
|
+
import{asType as t,createValidator as a}from"@enkaku/schema";import{verifyToken as i}from"@enkaku/token";import{computeMutationHash as o}from"@kubun/engine";import{DocumentID as n}from"@kubun/id";import{applyMutation as u}from"@kubun/mutation";import{documentMutation as e}from"@kubun/protocol";import{getGraphStore as r}from"@kubun/store-graph";let m=a(e);export async function applySyncMutations(a){let{db:e,mutationJWTs:s}=a,d=await r(e),l=0,c=0,h=0,p=0,_={store:d,validators:{}};for(let a of s){let e,r=o(a);if(await d.hasMutationHash(r)){p++;continue}try{let o=await i(a);e=t(m,o.payload)}catch{c++;continue}let s=e.sub,f=n.fromString(s).model.toString(),w=n.fromString(s);if(null==await d.getDocument(w)&&"change"===e.typ){await d.insertMutationLogEntry({mutation_hash:r,model_id:f,document_id:s,author_did:e.iss,hlc:e.hlc,mutation_jwt:a,status:"pending"}),h++;continue}try{if(await u(_,e),await d.insertMutationLogEntry({mutation_hash:r,model_id:f,document_id:s,author_did:e.iss,hlc:e.hlc,mutation_jwt:a,status:"applied"}),l++,"set"===e.typ)for(let a of(await d.getPendingMutations(s)))try{let o=await i(a.mutation_jwt),n=t(m,o.payload);await u(_,n),await d.updateMutationStatus(a.mutation_hash,"applied"),l++}catch{await d.updateMutationStatus(a.mutation_hash,"rejected"),c++}}catch{await d.insertMutationLogEntry({mutation_hash:r,model_id:f,document_id:s,author_did:e.iss,hlc:e.hlc,mutation_jwt:a,status:"rejected"}),c++}}return{applied:l,rejected:c,pending:h,skipped:p}}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { KubunDB } from '@kubun/db';
|
|
2
|
+
import type { Logger } from '@kubun/logger';
|
|
3
|
+
import type { SyncMerkleSyncParams, SyncMerkleSyncReceive, SyncMerkleSyncResult, SyncMerkleSyncSend } from '../protocol.js';
|
|
4
|
+
/**
|
|
5
|
+
* Minimal handler context shape for sync/merkle-sync channel.
|
|
6
|
+
* Matches the Enkaku ChannelHandlerContext at runtime.
|
|
7
|
+
*/
|
|
8
|
+
type MerkleSyncChannelContext = {
|
|
9
|
+
param: SyncMerkleSyncParams;
|
|
10
|
+
readable: ReadableStream<SyncMerkleSyncSend>;
|
|
11
|
+
writable: WritableStream<SyncMerkleSyncReceive>;
|
|
12
|
+
};
|
|
13
|
+
export type CreateMerkleSyncChannelHandlerParams = {
|
|
14
|
+
db: KubunDB;
|
|
15
|
+
logger: Logger;
|
|
16
|
+
};
|
|
17
|
+
export declare function createMerkleSyncChannelHandler(params: CreateMerkleSyncChannelHandlerParams): (ctx: MerkleSyncChannelContext) => Promise<SyncMerkleSyncResult>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{getGraphStore as e}from"@kubun/store-graph";import{applySyncMutations as t}from"./merkle-apply.js";import{buildMerkleTree as r,findDivergentBuckets as a,getTimeBuckets as n,SYNC_BATCH_SIZE as l}from"./merkle-tree.js";export function createMerkleSyncChannelHandler(i){let{db:o,logger:s}=i;return async i=>{let{scopes:c,excludedDocumentIDs:m,tree:u,direction:p}=i.param,y=i.writable.getWriter();s.info("sync/merkle-sync channel started",{scopes:c,excludedDocumentIDs:m,direction:p});let w=0,f=0,k=0;try{let h=await e(o),d=await h.getDocumentIDsForScope(c,m),g=await h.getMutationLogForDocuments(d),b=r(g);await y.write({type:"tree",tree:b.buckets});let v={root:u.root??"",buckets:u},W=a(b,v);w=W.length;let J=async()=>{if("pull"===p||"both"===p){let e=new Set(W),t=g.filter(t=>{let{minute:r}=n(t.hlc);return e.has(r)});for(let e=0;e<t.length;e+=l){let r=t.slice(e,e+l);await y.write({type:"mutations",mutationJWTs:r.map(e=>e.mutation_jwt)}),f+=r.length}await y.write({type:"complete"})}else await y.write({type:"complete"})},S=async()=>{let e=[],t=i.readable.getReader();try{for(;;){let{done:r,value:a}=await t.read();if(r)break;if("mutations"===a.type&&null!=a.mutationJWTs)e.push(...a.mutationJWTs);else if("complete"===a.type)break}}finally{t.releaseLock()}return e},[,T]=await Promise.all([J(),S()]);if(T.length>0){let e=await t({db:o,mutationJWTs:T});k=e.applied+e.pending,s.info("sync/merkle-sync: applied received mutations",{result:e})}}catch(e){return s.error("sync/merkle-sync channel error",{error:e}),{success:!1,divergentBuckets:0,mutationsSent:0,mutationsReceived:0}}finally{try{await y.close()}catch{}}return s.info("sync/merkle-sync channel completed",{divergentBuckets:w,mutationsSent:f,mutationsReceived:k}),{success:!0,divergentBuckets:w,mutationsSent:f,mutationsReceived:k}}}
|