@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.
Files changed (73) hide show
  1. package/lib/context/group.d.ts +4 -0
  2. package/lib/context/group.js +1 -0
  3. package/lib/context/join.js +1 -1
  4. package/lib/context/sync.js +1 -1
  5. package/lib/context/types.d.ts +7 -8
  6. package/lib/groups/broadcast-codec.d.ts +3 -0
  7. package/lib/groups/broadcast-service.d.ts +76 -0
  8. package/lib/groups/broadcast-service.js +1 -0
  9. package/lib/{spaces → groups}/broadcast.d.ts +9 -10
  10. package/lib/groups/broadcast.js +1 -0
  11. package/lib/groups/events.d.ts +38 -0
  12. package/lib/{spaces → groups}/events.js +1 -1
  13. package/lib/{spaces → groups}/invite-payload.d.ts +2 -2
  14. package/lib/groups/join-utils.d.ts +19 -0
  15. package/lib/groups/join-utils.js +1 -0
  16. package/lib/{spaces → groups}/manager.d.ts +56 -46
  17. package/lib/groups/manager.js +1 -0
  18. package/lib/groups/mls-group-handle.d.ts +4 -0
  19. package/lib/groups/mls-group-handle.js +1 -0
  20. package/lib/groups/mls-state.d.ts +42 -0
  21. package/lib/groups/mls-state.js +1 -0
  22. package/lib/hub/connection-pool.d.ts +19 -0
  23. package/lib/hub/connection-pool.js +1 -0
  24. package/lib/hub/group-channel.d.ts +51 -0
  25. package/lib/hub/group-channel.js +1 -0
  26. package/lib/hub/receive-handler.d.ts +36 -0
  27. package/lib/hub/receive-handler.js +1 -0
  28. package/lib/hub/relay-manager.d.ts +37 -0
  29. package/lib/hub/relay-manager.js +1 -0
  30. package/lib/hub/send-handler.d.ts +28 -0
  31. package/lib/hub/send-handler.js +1 -0
  32. package/lib/index.d.ts +14 -13
  33. package/lib/index.js +1 -1
  34. package/lib/protocol.d.ts +44 -10
  35. package/lib/protocol.js +1 -1
  36. package/lib/schema.d.ts +2 -2
  37. package/lib/schema.js +42 -42
  38. package/lib/sync/catalog-scope.js +1 -1
  39. package/lib/sync/handlers.d.ts +10 -0
  40. package/lib/sync/handlers.js +1 -1
  41. package/lib/sync/merkle-apply.js +1 -1
  42. package/lib/sync/merkle-channel.d.ts +18 -0
  43. package/lib/sync/merkle-channel.js +1 -0
  44. package/lib/sync/merkle-tree.d.ts +3 -1
  45. package/lib/sync/merkle-tree.js +1 -1
  46. package/lib/sync/peer-registry.d.ts +27 -8
  47. package/lib/sync/peer-registry.js +1 -1
  48. package/lib/sync/sync-client.d.ts +15 -13
  49. package/lib/sync/sync-client.js +1 -1
  50. package/lib/sync/sync-manager.d.ts +36 -11
  51. package/lib/sync/sync-manager.js +1 -1
  52. package/lib/types.d.ts +42 -40
  53. package/package.json +20 -16
  54. package/lib/context/space.d.ts +0 -4
  55. package/lib/context/space.js +0 -1
  56. package/lib/migrations.d.ts +0 -8
  57. package/lib/migrations.js +0 -1
  58. package/lib/spaces/broadcast-codec.d.ts +0 -3
  59. package/lib/spaces/broadcast-service.d.ts +0 -59
  60. package/lib/spaces/broadcast-service.js +0 -1
  61. package/lib/spaces/broadcast.js +0 -1
  62. package/lib/spaces/events.d.ts +0 -38
  63. package/lib/spaces/group-handle.d.ts +0 -3
  64. package/lib/spaces/group-handle.js +0 -1
  65. package/lib/spaces/join-utils.d.ts +0 -22
  66. package/lib/spaces/join-utils.js +0 -1
  67. package/lib/spaces/manager.js +0 -1
  68. package/lib/spaces/mls-state.d.ts +0 -27
  69. package/lib/spaces/mls-state.js +0 -1
  70. /package/lib/{spaces → groups}/broadcast-codec.js +0 -0
  71. /package/lib/{spaces → groups}/invite-payload.js +0 -0
  72. /package/lib/{spaces → groups}/mls-json.d.ts +0 -0
  73. /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 { createP2PMigrations, type MigrationContext } from './migrations.js';
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 { type MerkleSyncMessage, SyncClient, type SyncClientParams, type SyncScope, type SyncTransportProvider, } from './sync/sync-client.js';
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 { CatalogData, CircleData, CircleMemberData, DeleteCatalogData, DeleteCircleData, InviteToSpaceData, JoinSpaceData, LeaveSpaceData, P2PJoinRequestContext, RemoveCircleMemberData, RemoveSpaceMemberData, SpaceData, SpaceMemberData, SpaceRequestContext, SyncPluginAPI, SyncRequestContext, SyncSessionResult, UpdateCircleInput, UpdateSpaceInput, } from './types.js';
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 t}from"@enkaku/server";import{DirectTransports as r}from"@enkaku/transport";import{HLC as s}from"@kubun/hlc";import{createJoinContext as o}from"./context/join.js";import{createSpaceContext as n}from"./context/space.js";import{createSyncContext as a}from"./context/sync.js";import{createP2PMigrations as c}from"./migrations.js";import{createP2PSchemaExtension as i}from"./schema.js";import{createSpaceEventEmitter as p}from"./spaces/events.js";import{SpaceManager as m}from"./spaces/manager.js";import{createSyncHandlers as d}from"./sync/handlers.js";import{SyncManager as l}from"./sync/sync-manager.js";export{createP2PMigrations}from"./migrations.js";export{createP2PSchemaExtension}from"./schema.js";export{processBroadcast}from"./spaces/broadcast.js";export{deserializeBroadcast,serializeBroadcast}from"./spaces/broadcast-codec.js";export{BroadcastService}from"./spaces/broadcast-service.js";export{createFilteredGenerator,createSpaceEventEmitter}from"./spaces/events.js";export{restoreGroupHandle}from"./spaces/group-handle.js";export{decodeFullJoinRequest,decodeInvitePayload,decodeJoinRequest,encodeFullJoinRequest,encodeInvitePayload,encodeJoinRequest}from"./spaces/invite-payload.js";export{SpaceManager}from"./spaces/manager.js";export{replacer as mlsJSONReplacer,reviver as mlsJSONReviver}from"./spaces/mls-json.js";export{deserializeGroupState,serializeGroupState}from"./spaces/mls-state.js";export{resolveCatalogSyncScopes}from"./sync/catalog-scope.js";export{checkSyncDelegation}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 function createP2PPlugin(g){return y=>{let u,f,j=y.db.getDB(),h=y.db.adapter,v=y.identity,x=new s({nodeID:v.id}),P=new m({kysely:j,adapter:h,identity:v,getRandomID:y.runtime.getRandomID}),S=p(),w=new l({db:y.db,deployClusters:async e=>{let t=Object.values(e);await y.engine.deployGraph({clusters:t})},runtime:y.runtime,identity:y.identity,logger:y.getLogger("sync")}),R=d({db:y.db,logger:y.getLogger("sync-handlers")}),b=new Map,k=[],I={types:h.types,functions:h.functions};async function D(){return await j}let C={addPeer:e=>w.addPeer(e),removePeer:e=>w.removePeer(e),listPeers:()=>w.listPeers(),syncWithPeer:({peerDID:e,scopes:t,delegationTokens:r,knownModelIDs:s})=>w.merkleSyncWithPeer(e,t,r,{knownModelIDs:s}),getStatus:e=>w.getStatus(e),onSyncEvent:e=>w.onSyncEvent(e),createSyncTransport:function(e){let s=new r({signal:e}),o=new t({accessControl:!1,getRandomID:y.runtime.getRandomID,handlers:R,logger:y.getLogger("sync-server"),transports:[s.server],signal:e});return k.push(o),s.client}};if(g?.http){let r="string"==typeof g.http?g.http.replace(/^\//,""):"sync";C.syncReady=(async()=>{let s=await y.engine.getAPI("http");await s.listening,u=new e({allowedOrigin:["*"]}),f=new t({accessControl:!1,getRandomID:y.runtime.getRandomID,handlers:R,logger:y.getLogger("sync-http-server"),transports:[u]}),k.push(f),s.registerProtocol(r,u.fetch.bind(u))})()}return{name:"p2p",migrations:{name:"p2p",migrations:()=>c(I)},schemaExtension:e=>i(S),api:C,createContextFactory:()=>{let e={identity:v,spaceManager:P,syncManager:w,adapter:h,hlc:x,emitter:S,pendingJoinRequests:b,getDB:D,runtime:y.runtime,autoAcceptPeers:g?.autoAcceptPeers};return t=>({...n(t,e),...a(t,e),...o(t,e)})},dispose:async()=>{for(let e of k)await e.dispose();await u?.dispose(),await w.dispose()}}}}
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: "stream";
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 receive: {
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
- readonly divergentBuckets: {
139
- readonly type: "number";
140
- readonly description: "Number of divergent minute buckets found";
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 totalMutations: {
143
- readonly type: "number";
144
- readonly description: "Total mutations sent";
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:"stream",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"}}},required:["scopes","excludedDocumentIDs","tree"],additionalProperties:!1},receive:{type:"object",properties:{type:{type:"string",enum:["mutations","complete"]},mutationJWTs:{type:"array",items:{type:"string"},description:"Signed mutation JWTs for divergent time buckets"},divergentBuckets:{type:"number",description:"Number of divergent minute buckets found"},totalMutations:{type:"number",description:"Total mutations sent"}},required:["type"],additionalProperties:!1},result:{type:"object",properties:{success:{type:"boolean"},divergentBuckets:{type:"number"},mutationsSent:{type:"number"}},required:["success","divergentBuckets","mutationsSent"],additionalProperties:!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"},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 SpaceEventEmitter } from './spaces/events.js';
3
- export declare function createP2PSchemaExtension(emitter: SpaceEventEmitter): SchemaExtension;
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"./spaces/events.js";let a=`
2
- type PeerSpace {
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 PeerSpaceMember {
11
- spaceID: ID!
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
- spaceID: ID!
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 CreateSpaceInput {
41
+ input CreateGroupInput {
42
42
  name: String!
43
43
  description: String
44
44
  }
45
45
 
46
- type CreateSpacePayload {
47
- space: PeerSpace!
46
+ type CreateGroupPayload {
47
+ group: PeerGroup!
48
48
  }
49
49
 
50
- type InviteToSpacePayload {
50
+ type InviteToGroupPayload {
51
51
  invitePayload: String!
52
- spaceID: ID!
53
- spaceName: String!
52
+ groupID: ID!
53
+ groupName: String!
54
54
  }
55
55
 
56
- type JoinSpacePayload {
57
- space: PeerSpace
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
- space: PeerSpace
65
+ group: PeerGroup
66
66
  }
67
67
 
68
- type LeaveSpacePayload {
69
- spaceID: ID!
68
+ type LeaveGroupPayload {
69
+ groupID: ID!
70
70
  }
71
71
 
72
- type RemoveSpaceMemberPayload {
73
- spaceID: ID!
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 UpdateSpaceInput {
83
- spaceID: ID!
82
+ input UpdateGroupInput {
83
+ groupID: ID!
84
84
  name: String
85
85
  description: String
86
86
  }
87
87
 
88
- type UpdateSpacePayload {
89
- space: PeerSpace!
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
- spaces: [PeerSpace!]!
145
- space(id: ID!): PeerSpace
146
- spaceMembers(spaceID: ID!): [PeerSpaceMember!]!
147
- circles(spaceID: ID!): [PeerCircle!]!
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
- createSpace(input: CreateSpaceInput!): CreateSpacePayload!
154
- addSpaceMember(spaceID: ID!, memberDID: String!, role: String!): PeerSpaceMember!
155
- createCircle(spaceID: ID!, name: String!, description: String): PeerCircle!
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
- inviteToSpace(spaceID: ID!, joinRequest: String!): InviteToSpacePayload!
159
- joinSpace(invitePayload: String!, joinRequestPayload: String!): JoinSpacePayload!
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
- leaveSpace(spaceID: ID!): LeaveSpacePayload!
163
- removeSpaceMember(spaceID: ID!, memberDID: String!): RemoveSpaceMemberPayload!
162
+ leaveGroup(groupID: ID!): LeaveGroupPayload!
163
+ removeGroupMember(groupID: ID!, memberDID: String!): RemoveGroupMemberPayload!
164
164
  updateCatalog(input: UpdateCatalogInput!): PeerCatalog!
165
- updateSpace(input: UpdateSpaceInput!): UpdateSpacePayload!
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
- spaceJoined: PeerSpace!
176
- spaceLeft: PeerSpace!
177
- spaceDataChanged(spaceID: ID!): PeerSpace!
178
- spaceMemberJoined(spaceID: ID!): PeerSpaceMember!
179
- spaceMemberLeft(spaceID: ID!): PeerSpaceMember!
180
- circleCreated(spaceID: ID!): PeerCircle!
181
- circleDeleted(spaceID: ID!): PeerCircle!
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(r){return{sdl:a,resolvers:{queryFields:{spaces:async(e,a,r)=>await r.spaceGetSpaces(),space:async(e,a,r)=>await r.spaceGetSpace(a.id),spaceMembers:async(e,a,r)=>await r.spaceGetSpaceMembers(a.spaceID),circles:async(e,a,r)=>await r.spaceGetCircles(a.spaceID),syncPeers:async(e,a,r)=>await r.syncListPeers(),syncStatus:(e,a,r)=>{let c=r.syncGetStatus();return{...c,lastSyncByPeer:JSON.stringify(c.lastSyncByPeer)}}},mutationFields:{createSpace:async(e,a,r)=>{let c=a.input;return{space:await r.spaceCreateSpace(c)}},addSpaceMember:async(e,a,r)=>await r.spaceAddSpaceMember(a.spaceID,a.memberDID,a.role),createCircle:async(e,a,r)=>await r.spaceCreateCircle({spaceID:a.spaceID,name:a.name,description:a.description}),addCircleMember:async(e,a,r)=>await r.spaceAddCircleMember(a.circleID,a.memberDID,a.role),createCatalog:async(e,a,r)=>await r.spaceCreateCatalog({name:a.name,description:a.description,filterCriteria:a.filterCriteria}),inviteToSpace:async(e,a,r)=>await r.spaceInviteToSpace(a.spaceID,a.joinRequest),joinSpace:async(e,a,r)=>await r.spaceJoinSpace(a.invitePayload,a.joinRequestPayload),prepareJoinRequest:async(e,a,r)=>await r.p2pPrepareJoinRequest(),completeJoin:async(e,a,r)=>await r.p2pCompleteJoin(a.invitePayload),leaveSpace:async(e,a,r)=>await r.spaceLeaveSpace(a.spaceID),removeSpaceMember:async(e,a,r)=>await r.spaceRemoveSpaceMember(a.spaceID,a.memberDID),updateCatalog:async(e,a,r)=>{let c=a.input;return await r.spaceUpdateCatalog(c)},updateSpace:async(e,a,r)=>{let c=a.input;return{space:await r.spaceUpdateSpace(c)}},updateCircle:async(e,a,r)=>{let c=a.input;return{circle:await r.spaceUpdateCircle(c)}},deleteCatalog:async(e,a,r)=>await r.spaceDeleteCatalog(a.catalogID),deleteCircle:async(e,a,r)=>await r.spaceDeleteCircle(a.circleID),removeCircleMember:async(e,a,r)=>await r.spaceRemoveCircleMember(a.circleID,a.memberDID),addSyncPeer:async(e,a,r)=>(await r.syncAddPeer({peerDID:a.peerDID,endpoint:a.endpoint,mode:a.mode??"on-demand",allowedUsers:{all:!0},priority:a.priority??"0",trustLevel:a.trustLevel??"trusted"}),!0),removeSyncPeer:async(e,a,r)=>(await r.syncRemovePeer(a.peerDID),{success:!0})},subscriptionFields:{spaceJoined:{resolve:e=>e,subscribe:()=>e(r,"spaceJoined")},spaceLeft:{resolve:e=>e,subscribe:()=>e(r,"spaceLeft")},spaceDataChanged:{resolve:e=>e,subscribe:(a,c)=>e(r,"spaceDataChanged",e=>e.spaceID===c.spaceID)},spaceMemberJoined:{resolve:e=>e,subscribe:(a,c)=>e(r,"spaceMemberJoined",e=>e.spaceID===c.spaceID)},spaceMemberLeft:{resolve:e=>e,subscribe:(a,c)=>e(r,"spaceMemberLeft",e=>e.spaceID===c.spaceID)},circleCreated:{resolve:e=>e,subscribe:(a,c)=>e(r,"circleCreated",e=>e.spaceID===c.spaceID)},circleDeleted:{resolve:e=>e,subscribe:(a,c)=>e(r,"circleDeleted",e=>e.spaceID===c.spaceID)},circleDataChanged:{resolve:e=>e,subscribe:(a,c)=>e(r,"circleDataChanged",e=>e.circleID===c.circleID)},circleMemberAdded:{resolve:e=>e,subscribe:(a,c)=>e(r,"circleMemberAdded",e=>e.circleID===c.circleID)},circleMemberRemoved:{resolve:e=>e,subscribe:(a,c)=>e(r,"circleMemberRemoved",e=>e.circleID===c.circleID)},circleCatalogsChanged:{resolve:e=>e,subscribe:(a,c)=>e(r,"circleCatalogsChanged",e=>e.circleID===c.circleID)}}}}}
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(e,r,o){let l=new Set,t=new Set,a=!1;for(let o of r){let r=await e.resolveCatalogScope(o);if(null!=r.models)for(let e of r.models)l.add(e);if(null!=r.owners)for(let e of(a=!0,r.owners))t.add(e)}let s=Array.from(l),n=a?Array.from(t):void 0,d=new Set(o??[]),f=new Set,i=new Set;return await Promise.all(s.map(async r=>{let o=await e.getClusterForModel(r);null!=o&&(f.add(o),d.has(r)||i.add(o))})),{modelIDs:s,owners:n,requiredClusterIDs:Array.from(f),missingClusterIDs:Array.from(i)}}
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)}}
@@ -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
  */
@@ -1 +1 @@
1
- import{checkCapability as e}from"@enkaku/capability";import{resolveCatalogSyncScopes as t}from"./catalog-scope.js";import{buildMerkleTree as n,findDivergentBuckets as r,getTimeBuckets as s}from"./merkle-tree.js";export async function checkSyncDelegation(t,n,r){if(!r||0===r.length)return!1;let s=["*","urn:kubun:user:*",`urn:kubun:user:${n}`],l="document/read";for(let o of s)try{return await e({act:l,res:o},{iss:t,sub:n,cap:r}),!0}catch{}for(let o of r)for(let r of s)try{return await e({act:l,res:r},{iss:t,sub:n,cap:o}),!0}catch{}return!1}export function createSyncHandlers(e){let{db:l,logger:o}=e;return{"sync/negotiate":async e=>{let n,{scopes:r,delegationTokens:s,catalogIDs:a,knownModelIDs:c}=e.param,i=e.message.payload,u=i.sub||i.iss;o.debug("sync/negotiate requested",{scopes:r,catalogIDs:a,viewerDID:u});let g=[];if(null!=a&&a.length>0){let e=await t(l,a,c);if(e.modelIDs.length>0)if(null!=e.owners)for(let t of e.modelIDs)for(let n of e.owners){let e=u===n,r=!e&&await checkSyncDelegation(u,n,s);(e||r)&&g.push({modelID:t,ownerDID:n})}else for(let t of e.modelIDs)for(let e of(await l.getDistinctOwnersForModel(t))){let n=u===e,r=!n&&await checkSyncDelegation(u,e,s);(n||r)&&g.push({modelID:t,ownerDID:e})}e.missingClusterIDs.length>0&&(n=await l.getClusters(e.missingClusterIDs))}if(null!=r)for(let e of r){let t=u===e.ownerDID,n=!t&&await checkSyncDelegation(u,e.ownerDID,s);t||n?g.push(e):o.debug("sync/negotiate: scope rejected",{scope:e,viewerDID:u})}let m=new Set,f=g.filter(e=>{let t=`${e.modelID}:${e.ownerDID}`;return!m.has(t)&&(m.add(t),!0)});return o.info("sync/negotiate completed",{requested:(r?.length??0)+(a?.length??0),accepted:f.length,missingClusters:null!=n?Object.keys(n).length:0}),{acceptedScopes:f,excludedDocumentIDs:[],...null!=n&&{missingClusters:n}}},"sync/merkle-sync":async e=>{let{scopes:t,excludedDocumentIDs:a,tree:c}=e.param,i=e.writable.getWriter();o.info("sync/merkle-sync started",{scopes:t,excludedDocumentIDs:a});let u=0,g=0;try{let e=await l.getDocumentIDsForScope(t,a);if(0===e.length)return await i.write({type:"complete",divergentBuckets:0,totalMutations:0}),{success:!0,divergentBuckets:0,mutationsSent:0};let m=await l.getMutationLogForDocuments(e),f=n(m),y={root:c.root??"",buckets:c},d=r(f,y);if(u=d.length,0===d.length)return o.info("sync/merkle-sync: no divergent buckets, already in sync"),await i.write({type:"complete",divergentBuckets:0,totalMutations:0}),{success:!0,divergentBuckets:0,mutationsSent:0};let p=new Set(d),w=m.filter(e=>{let{minute:t}=s(e.hlc);return p.has(t)});for(let e=0;e<w.length;e+=1e3){let t=w.slice(e,e+1e3);await i.write({type:"mutations",mutationJWTs:t.map(e=>e.mutation_jwt)}),g+=t.length}await i.write({type:"complete",divergentBuckets:u,totalMutations:g})}catch(e){throw o.error("sync/merkle-sync error",{error:e}),e}finally{try{await i.close()}catch{}}return o.info("sync/merkle-sync completed",{divergentBuckets:u,mutationsSent:g}),{success:!0,divergentBuckets:u,mutationsSent:g}}}}
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})}}
@@ -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 r=a(e);export async function applySyncMutations(a){let{db:e,mutationJWTs:m}=a,s=0,d=0,l=0,c=0,h={db:e,validators:{}};for(let a of m){let m,p=o(a);if(await e.hasMutationHash(p)){c++;continue}try{let o=await i(a);m=t(r,o.payload)}catch{d++;continue}let _=m.sub,f=n.fromString(_).model.toString(),w=n.fromString(_);if(null==await e.getDocument(w)&&"change"===m.typ){await e.insertMutationLogEntry({mutation_hash:p,model_id:f,document_id:_,author_did:m.iss,hlc:m.hlc,mutation_jwt:a,status:"pending"}),l++;continue}try{if(await u(h,m),await e.insertMutationLogEntry({mutation_hash:p,model_id:f,document_id:_,author_did:m.iss,hlc:m.hlc,mutation_jwt:a,status:"applied"}),s++,"set"===m.typ)for(let a of(await e.getPendingMutations(_)))try{let o=await i(a.mutation_jwt),n=t(r,o.payload);await u(h,n),await e.updateMutationStatus(a.mutation_hash,"applied"),s++}catch{await e.updateMutationStatus(a.mutation_hash,"rejected"),d++}}catch{await e.insertMutationLogEntry({mutation_hash:p,model_id:f,document_id:_,author_did:m.iss,hlc:m.hlc,mutation_jwt:a,status:"rejected"}),d++}}return{applied:s,rejected:d,pending:l,skipped:c}}
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}}}