@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
@@ -1,8 +1,10 @@
1
- import type { MutationLogEntry } from '@kubun/db';
1
+ import type { MutationLogEntry } from '@kubun/store-graph';
2
2
  export type MerkleTree = {
3
3
  root: string;
4
4
  buckets: Record<string, string>;
5
5
  };
6
+ /** Maximum number of mutation JWTs to send in a single channel message. */
7
+ export declare const SYNC_BATCH_SIZE = 1000;
6
8
  export declare function getTimeBuckets(hlc: string): {
7
9
  year: string;
8
10
  month: string;
@@ -1 +1 @@
1
- import{blake3 as t}from"@noble/hashes/blake3.js";let e=[4,7,10,16],o=new TextEncoder;export function getTimeBuckets(t){return{year:t.slice(0,4),month:t.slice(0,7),day:t.slice(0,10),minute:t.slice(0,16)}}function r(e){return Array.from(t(o.encode(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}export function buildMerkleTree(t){if(0===t.length)return{root:"",buckets:{}};let e=new Map;for(let o of t){let{minute:t}=getTimeBuckets(o.hlc),r=e.get(t);null==r&&(r=[],e.set(t,r)),r.push(o)}let o={};for(let[t,n]of e){let e=[...n].sort((t,e)=>t.mutation_hash<e.mutation_hash?-1:+(t.mutation_hash>e.mutation_hash)).map(t=>t.mutation_hash).join("\n");o[t]=r(e)}for(let t of[10,7,4]){let e=10===t?16:7===t?10:7,n=new Map;for(let r of Object.keys(o)){if(r.length!==e)continue;let o=r.slice(0,t),s=n.get(o);null==s&&(s=[],n.set(o,s)),s.push(r)}for(let[t,e]of n){let n=e.sort().map(t=>`${t}\0${o[t]}`).join("\n");o[t]=r(n)}}let n=r(Object.keys(o).filter(t=>4===t.length).sort().map(t=>`${t}\0${o[t]}`).join("\n"));return o.root=n,{root:n,buckets:o}}export function findDivergentBuckets(t,o){if(t.root===o.root)return[];if(""===t.root)return Object.keys(o.buckets).filter(t=>16===t.length).sort();if(""===o.root)return Object.keys(t.buckets).filter(t=>16===t.length).sort();let r=[],n=new Set([...Object.keys(t.buckets),...Object.keys(o.buckets)]);return!function s(l,i){let u=e[i];for(let c of[...n].filter(t=>t.length===u&&(""===l||t.startsWith(l))).sort())t.buckets[c]!==o.buckets[c]&&(i===e.length-1?r.push(c):s(c,i+1))}("",0),r.sort()}
1
+ import{blake3 as t}from"@noble/hashes/blake3.js";export const SYNC_BATCH_SIZE=1e3;let e=[4,7,10,16],o=new TextEncoder;export function getTimeBuckets(t){return{year:t.slice(0,4),month:t.slice(0,7),day:t.slice(0,10),minute:t.slice(0,16)}}function r(e){return Array.from(t(o.encode(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}export function buildMerkleTree(t){if(0===t.length)return{root:"",buckets:{}};let e=new Map;for(let o of t){let{minute:t}=getTimeBuckets(o.hlc),r=e.get(t);null==r&&(r=[],e.set(t,r)),r.push(o)}let o={};for(let[t,n]of e){let e=[...n].sort((t,e)=>t.mutation_hash<e.mutation_hash?-1:+(t.mutation_hash>e.mutation_hash)).map(t=>t.mutation_hash).join("\n");o[t]=r(e)}for(let t of[10,7,4]){let e=10===t?16:7===t?10:7,n=new Map;for(let r of Object.keys(o)){if(r.length!==e)continue;let o=r.slice(0,t),s=n.get(o);null==s&&(s=[],n.set(o,s)),s.push(r)}for(let[t,e]of n){let n=e.sort().map(t=>`${t}\0${o[t]}`).join("\n");o[t]=r(n)}}let n=r(Object.keys(o).filter(t=>4===t.length).sort().map(t=>`${t}\0${o[t]}`).join("\n"));return o.root=n,{root:n,buckets:o}}export function findDivergentBuckets(t,o){if(t.root===o.root)return[];if(""===t.root)return Object.keys(o.buckets).filter(t=>16===t.length).sort();if(""===o.root)return Object.keys(t.buckets).filter(t=>16===t.length).sort();let r=[],n=new Set([...Object.keys(t.buckets),...Object.keys(o.buckets)]);return!function s(l,i){let u=e[i];for(let c of[...n].filter(t=>t.length===u&&(""===l||t.startsWith(l))).sort())t.buckets[c]!==o.buckets[c]&&(i===e.length-1?r.push(c):s(c,i+1))}("",0),r.sort()}
@@ -1,5 +1,6 @@
1
1
  import type { Runtime } from '@enkaku/runtime';
2
- import type { KubunDB } from '@kubun/db';
2
+ import type { StoreProvider } from '@kubun/db';
3
+ import type { SyncDirection } from '../protocol.js';
3
4
  export type PeerConfig = {
4
5
  peerDID: string;
5
6
  endpoint: string;
@@ -9,6 +10,7 @@ export type PeerConfig = {
9
10
  };
10
11
  priority: string;
11
12
  trustLevel: 'trusted' | 'restricted';
13
+ allowedDirection?: SyncDirection;
12
14
  };
13
15
  export type PeerConfigWithID = PeerConfig & {
14
16
  id: string;
@@ -16,16 +18,33 @@ export type PeerConfigWithID = PeerConfig & {
16
18
  updatedAt: number;
17
19
  };
18
20
  export type PeerRegistryParams = {
19
- db: KubunDB;
20
21
  runtime: Runtime;
21
22
  };
22
23
  export declare class PeerRegistry {
23
24
  #private;
24
25
  constructor(params: PeerRegistryParams);
25
- addPeer(config: PeerConfig): Promise<void>;
26
- getPeer(peerDID: string): Promise<PeerConfigWithID | undefined>;
27
- listPeers(): Promise<Array<PeerConfigWithID>>;
28
- updatePeer(peerDID: string, updates: Partial<PeerConfig>): Promise<void>;
29
- removePeer(peerDID: string): Promise<void>;
30
- isPeerAllowed(peerDID: string): Promise<boolean>;
26
+ addPeer(params: {
27
+ config: PeerConfig;
28
+ stores: StoreProvider;
29
+ }): Promise<void>;
30
+ getPeer(params: {
31
+ peerDID: string;
32
+ stores: StoreProvider;
33
+ }): Promise<PeerConfigWithID | undefined>;
34
+ listPeers(params: {
35
+ stores: StoreProvider;
36
+ }): Promise<Array<PeerConfigWithID>>;
37
+ updatePeer(params: {
38
+ peerDID: string;
39
+ updates: Partial<PeerConfig>;
40
+ stores: StoreProvider;
41
+ }): Promise<void>;
42
+ removePeer(params: {
43
+ peerDID: string;
44
+ stores: StoreProvider;
45
+ }): Promise<void>;
46
+ isPeerAllowed(params: {
47
+ peerDID: string;
48
+ stores: StoreProvider;
49
+ }): Promise<boolean>;
31
50
  }
@@ -1 +1 @@
1
- function e(e){var t;return{id:e.id,peerDID:e.peer_did,endpoint:e.endpoint,mode:e.mode,allowedUsers:"string"==typeof(t=e.allowed_users)?JSON.parse(t):t,priority:e.priority,trustLevel:e.trust_level,createdAt:e.created_at,updatedAt:e.updated_at}}export class PeerRegistry{#e;#t;constructor(e){this.#e=e.db,this.#t=e.runtime}async addPeer(e){let t=await this.#e.getDB();await t.insertInto("kubun_sync_peers").values({id:this.#t.getRandomID(),peer_did:e.peerDID,endpoint:e.endpoint,mode:e.mode,allowed_users:JSON.stringify(e.allowedUsers),priority:e.priority,trust_level:e.trustLevel,config:JSON.stringify({}),created_at:Date.now(),updated_at:Date.now()}).onConflict(t=>t.column("peer_did").doUpdateSet({endpoint:e.endpoint,mode:e.mode,allowed_users:JSON.stringify(e.allowedUsers),priority:e.priority,trust_level:e.trustLevel,updated_at:Date.now()})).execute()}async getPeer(t){let r=await this.#e.getDB(),i=await r.selectFrom("kubun_sync_peers").selectAll().where("peer_did","=",t).executeTakeFirst();if(i)return e(i)}async listPeers(){let t=await this.#e.getDB();return(await t.selectFrom("kubun_sync_peers").selectAll().execute()).map(t=>e(t))}async updatePeer(e,t){let r=await this.getPeer(e);if(!r)throw Error(`Peer ${e} not found`);let i={...r,...t},s=await this.#e.getDB();await s.updateTable("kubun_sync_peers").set({endpoint:i.endpoint,mode:i.mode,allowed_users:JSON.stringify(i.allowedUsers),priority:i.priority,trust_level:i.trustLevel,config:JSON.stringify({}),updated_at:Date.now()}).where("peer_did","=",e).execute()}async removePeer(e){let t=await this.#e.getDB();await t.deleteFrom("kubun_sync_peers").where("peer_did","=",e).execute()}async isPeerAllowed(e){return void 0!==await this.getPeer(e)}}
1
+ import{getP2PStore as e}from"@kubun/store-p2p";function t(e){var t,r;let i="string"==typeof(t=e.config)?JSON.parse(t):t??{};return{id:e.id,peerDID:e.peer_did,endpoint:e.endpoint,mode:e.mode,allowedUsers:"string"==typeof(r=e.allowed_users)?JSON.parse(r):r,priority:e.priority,trustLevel:e.trust_level,allowedDirection:i.allowedDirection,createdAt:e.created_at,updatedAt:e.updated_at}}export class PeerRegistry{#e;constructor(e){this.#e=e.runtime}async addPeer(t){let r=await e(t.stores);await r.addSyncPeer({id:this.#e.getRandomID(),peer_did:t.config.peerDID,endpoint:t.config.endpoint,mode:t.config.mode,allowed_users:t.config.allowedUsers,priority:t.config.priority,trust_level:t.config.trustLevel,config:{allowedDirection:t.config.allowedDirection},created_at:Date.now(),updated_at:Date.now()})}async getPeer(r){let i=await e(r.stores),o=await i.getSyncPeer(r.peerDID);if(o)return t(o)}async listPeers(r){let i=await e(r.stores);return(await i.listSyncPeers()).map(e=>t(e))}async updatePeer(t){let r=await this.getPeer({peerDID:t.peerDID,stores:t.stores});if(!r)throw Error(`Peer ${t.peerDID} not found`);let i={...r,...t.updates},o=await e(t.stores);await o.updateSyncPeer(t.peerDID,{endpoint:i.endpoint,mode:i.mode,allowed_users:i.allowedUsers,priority:i.priority,trust_level:i.trustLevel,config:{allowedDirection:i.allowedDirection}})}async removePeer(t){let r=await e(t.stores);await r.removeSyncPeer(t.peerDID)}async isPeerAllowed(e){return void 0!==await this.getPeer({peerDID:e.peerDID,stores:e.stores})}}
@@ -3,7 +3,7 @@ import type { ClientTransportOf } from '@enkaku/protocol';
3
3
  import type { Runtime } from '@enkaku/runtime';
4
4
  import type { SigningIdentity } from '@enkaku/token';
5
5
  import type { Logger } from '@kubun/logger';
6
- import type { SyncProtocol } from '../protocol.js';
6
+ import type { SyncDirection, SyncProtocol } from '../protocol.js';
7
7
  /**
8
8
  * Provider for sync-specific transport to a peer's sync server.
9
9
  * The p2p plugin exposes this via `createSyncTransport()`.
@@ -21,14 +21,6 @@ export type SyncScope = {
21
21
  modelID: string;
22
22
  ownerDID: string;
23
23
  };
24
- export type MerkleSyncMessage = {
25
- type: 'mutations';
26
- mutationJWTs: Array<string>;
27
- } | {
28
- type: 'complete';
29
- divergentBuckets?: number;
30
- totalMutations?: number;
31
- };
32
24
  /**
33
25
  * Client for connecting to peer servers for sync operations.
34
26
  * Supports both direct (in-process) and HTTP transports.
@@ -48,21 +40,31 @@ export declare class SyncClient {
48
40
  /**
49
41
  * Negotiate sync scopes with peer.
50
42
  */
51
- negotiate(client: Client<SyncProtocol>, scopes: Array<SyncScope>, delegationTokens?: Array<string>, knownModelIDs?: Array<string>): Promise<{
43
+ negotiate(client: Client<SyncProtocol>, scopes: Array<SyncScope>, delegationTokens?: Array<string>, knownModelIDs?: Array<string>, direction?: SyncDirection): Promise<{
52
44
  acceptedScopes: Array<SyncScope>;
53
45
  excludedDocumentIDs: Array<string>;
54
46
  missingClusters?: Record<string, unknown>;
47
+ direction?: SyncDirection;
55
48
  }>;
56
49
  /**
57
- * Perform Merkle tree-based sync with peer.
58
- * Sends local tree, receives divergent mutation JWTs.
50
+ * Perform Merkle tree-based sync with peer using bidirectional channel.
51
+ * Sends local tree and divergent mutations, receives remote mutations.
59
52
  */
60
53
  merkleSync(client: Client<SyncProtocol>, params: {
61
54
  scopes: Array<SyncScope>;
62
55
  excludedDocumentIDs: Array<string>;
63
- tree: Record<string, string>;
56
+ localTree: {
57
+ root: string;
58
+ buckets: Record<string, string>;
59
+ };
60
+ direction: SyncDirection;
61
+ localEntries: Array<{
62
+ hlc: string;
63
+ mutation_jwt: string;
64
+ }>;
64
65
  }): Promise<{
65
66
  mutationJWTs: Array<string>;
66
67
  divergentBuckets: number;
68
+ mutationsSent: number;
67
69
  }>;
68
70
  }
@@ -1 +1 @@
1
- import{Client as e}from"@enkaku/client";import{ClientTransport as t}from"@enkaku/http-client-transport";import{createArraySink as r}from"@enkaku/stream";export class SyncClient{#e;#t;#r;#n;constructor(e){this.#e=e.runtime,this.#t=e.identity,this.#r=e.logger,this.#n=e.serverResolver}async connect(e,t){if(e.startsWith("direct://"))return this.#s(e);if(e.startsWith("http://")||e.startsWith("https://"))return this.#i(e,t);throw Error(`Unsupported endpoint scheme: ${e}`)}#i(r,n){let s=new t({url:r,fetch:this.#e.fetch});return new e({runtime:this.#e,identity:this.#t,logger:this.#r.getChild("sync-client"),serverID:n,transport:s})}#s(t){let r=t.replace("direct://","");if(!this.#n)throw Error("Server resolver not configured for direct transport");let n=this.#n(r);if(!n)throw Error(`Server not found: ${r}`);let s=n.createSyncTransport();return new e({runtime:this.#e,identity:this.#t,logger:this.#r.getChild("sync-client"),serverID:r,transport:s})}async negotiate(e,t,r=[],n){this.#r.debug("Negotiate: sending {scopeCount} scopes",{scopeCount:t.length});let s=await e.request("sync/negotiate",{param:{scopes:t,delegationTokens:r,knownModelIDs:n}});return this.#r.debug("Negotiate: accepted {acceptedCount} scopes, excluded {excludedCount} docs",{acceptedCount:s.acceptedScopes.length,excludedCount:s.excludedDocumentIDs.length}),s}async merkleSync(e,t){this.#r.debug("MerkleSync: creating stream with {buckets} local buckets",{buckets:Object.keys(t.tree).length});let[n,s]=r(),i=e.createStream("sync/merkle-sync",{param:{scopes:t.scopes,excludedDocumentIDs:t.excludedDocumentIDs,tree:t.tree}}),o=i.readable.pipeTo(n);this.#r.debug("MerkleSync: awaiting stream completion"),await Promise.all([i,o]),this.#r.debug("MerkleSync: stream completed, collecting messages");let c=await s,l=[],a=0;for(let e of c)"mutations"===e.type?l.push(...e.mutationJWTs):"complete"===e.type&&(a=e.divergentBuckets??0);return{mutationJWTs:l,divergentBuckets:a}}}
1
+ import{Client as e}from"@enkaku/client";import{ClientTransport as t}from"@enkaku/http-client-transport";import{findDivergentBuckets as r,getTimeBuckets as n,SYNC_BATCH_SIZE as i}from"./merkle-tree.js";export class SyncClient{#e;#t;#r;#n;constructor(e){this.#e=e.runtime,this.#t=e.identity,this.#r=e.logger,this.#n=e.serverResolver}async connect(e,t){if(e.startsWith("direct://"))return this.#i(e);if(e.startsWith("http://")||e.startsWith("https://"))return this.#s(e,t);throw Error(`Unsupported endpoint scheme: ${e}`)}#s(r,n){let i=new t({url:r,fetch:this.#e.fetch});return new e({runtime:this.#e,identity:this.#t,logger:this.#r.getChild("sync-client"),serverID:n,transport:i})}#i(t){let r=t.replace("direct://","");if(!this.#n)throw Error("Server resolver not configured for direct transport");let n=this.#n(r);if(!n)throw Error(`Server not found: ${r}`);let i=n.createSyncTransport();return new e({runtime:this.#e,identity:this.#t,logger:this.#r.getChild("sync-client"),serverID:r,transport:i})}async negotiate(e,t,r=[],n,i){this.#r.debug("Negotiate: sending {scopeCount} scopes",{scopeCount:t.length});let s=await e.request("sync/negotiate",{param:{scopes:t,delegationTokens:r,knownModelIDs:n,direction:i}});return this.#r.debug("Negotiate: accepted {acceptedCount} scopes, excluded {excludedCount} docs",{acceptedCount:s.acceptedScopes.length,excludedCount:s.excludedDocumentIDs.length}),s}async merkleSync(e,t){let{scopes:s,excludedDocumentIDs:o,localTree:c,direction:l,localEntries:a}=t;this.#r.debug("MerkleSync: creating channel with {buckets} local buckets",{buckets:Object.keys(c.buckets).length});let u=e.createChannel("sync/merkle-sync",{param:{scopes:s,excludedDocumentIDs:o,tree:c.buckets,direction:l}}),h=u.readable.getReader(),d=await h.read();if(d.done)throw Error("MerkleSync: channel closed before receiving remote tree");let g=d.value;if("tree"!==g.type)throw Error(`MerkleSync: expected 'tree' message, got '${g.type}'`);let p=r(c,{root:g.tree?.root??"",buckets:g.tree??{}}),m=p.length;this.#r.debug("MerkleSync: found {count} divergent buckets",{count:m});let y=0,f=async()=>{if("push"===l||"both"===l){let e=new Set(p),t=a.filter(t=>{let{minute:r}=n(t.hlc);return e.has(r)});for(let e=0;e<t.length;e+=i){let r=t.slice(e,e+i);await u.send({type:"mutations",mutationJWTs:r.map(e=>e.mutation_jwt)}),y+=r.length}}await u.send({type:"complete"})},k=async()=>{let e=[];for(;;){let{done:t,value:r}=await h.read();if(t)break;if("mutations"===r.type&&null!=r.mutationJWTs)e.push(...r.mutationJWTs);else if("complete"===r.type)break}return h.releaseLock(),e},[,v]=await Promise.all([f(),k()]),w=await u;if(null!=w&&!w.success)throw Error("MerkleSync: responder reported failure");return this.#r.debug("MerkleSync: completed with {received} received, {sent} sent",{received:v.length,sent:y}),{mutationJWTs:v,divergentBuckets:m,mutationsSent:y}}}
@@ -1,8 +1,9 @@
1
1
  import type { Runtime } from '@enkaku/runtime';
2
2
  import { type Identity, type SigningIdentity } from '@enkaku/token';
3
- import type { KubunDB } from '@kubun/db';
3
+ import type { StoreProvider } from '@kubun/db';
4
4
  import type { Logger } from '@kubun/logger';
5
- import { type PeerConfig, type PeerConfigWithID } from './peer-registry.js';
5
+ import type { SyncDirection } from '../protocol.js';
6
+ import { type PeerConfig, type PeerConfigWithID, PeerRegistry } from './peer-registry.js';
6
7
  import { type SyncScope, type SyncTransportProvider } from './sync-client.js';
7
8
  export type SyncSessionInfo = {
8
9
  peerID: string;
@@ -23,19 +24,24 @@ export type SyncEvent = {
23
24
  reason?: string;
24
25
  };
25
26
  export type SyncManagerParams = {
26
- db: KubunDB;
27
27
  deployClusters?: (clusters: Record<string, unknown>) => Promise<void>;
28
28
  runtime: Runtime;
29
29
  identity: Identity;
30
30
  logger: Logger;
31
31
  serverResolver?: (serverID: string) => SyncTransportProvider | undefined;
32
32
  };
33
- export type MerkleSyncOptions = {
33
+ export type MerkleSyncParams = {
34
+ peerDID: string;
35
+ scopes: Array<SyncScope>;
36
+ stores: StoreProvider;
37
+ delegationTokens?: Array<string>;
34
38
  knownModelIDs?: Array<string>;
39
+ direction?: SyncDirection;
35
40
  };
36
41
  export declare class SyncManager {
37
42
  #private;
38
43
  constructor(params: SyncManagerParams);
44
+ get peerRegistry(): PeerRegistry;
39
45
  /**
40
46
  * Configure the signing identity used for sync operations.
41
47
  */
@@ -44,18 +50,37 @@ export declare class SyncManager {
44
50
  * Configure the server resolver for direct transport connections.
45
51
  */
46
52
  setServerResolver(resolver: (serverID: string) => SyncTransportProvider | undefined): void;
47
- addPeer(config: PeerConfig): Promise<void>;
48
- removePeer(peerDID: string): Promise<void>;
49
- updatePeerConfig(peerDID: string, updates: Partial<PeerConfig>): Promise<void>;
50
- listPeers(): Promise<Array<PeerConfigWithID>>;
51
- getPeer(peerDID: string): Promise<PeerConfigWithID | undefined>;
52
- merkleSyncWithPeer(peerDID: string, scopes: Array<SyncScope>, delegationTokens?: Array<string>, options?: MerkleSyncOptions): Promise<{
53
+ addPeer(params: {
54
+ config: PeerConfig;
55
+ stores: StoreProvider;
56
+ }): Promise<void>;
57
+ removePeer(params: {
58
+ peerDID: string;
59
+ stores: StoreProvider;
60
+ }): Promise<void>;
61
+ updatePeerConfig(params: {
62
+ peerDID: string;
63
+ updates: Partial<PeerConfig>;
64
+ stores: StoreProvider;
65
+ }): Promise<void>;
66
+ listPeers(params: {
67
+ stores: StoreProvider;
68
+ }): Promise<Array<PeerConfigWithID>>;
69
+ getPeer(params: {
70
+ peerDID: string;
71
+ stores: StoreProvider;
72
+ }): Promise<PeerConfigWithID | undefined>;
73
+ merkleSyncWithPeer(params: MerkleSyncParams): Promise<{
53
74
  sessionID: string;
54
75
  divergentBuckets: number;
55
76
  messagesReceived: number;
77
+ messagesSent: number;
56
78
  missingClusters?: Record<string, unknown>;
57
79
  }>;
58
- getStatus(peerDID?: string): SyncStatus;
80
+ getStatus(params: {
81
+ peerDID?: string;
82
+ stores: StoreProvider;
83
+ }): SyncStatus;
59
84
  onSyncEvent(callback: (event: SyncEvent) => void): () => void;
60
85
  dispose(): Promise<void>;
61
86
  }
@@ -1 +1 @@
1
- import{EventEmitter as e}from"@enkaku/event";import{isSigningIdentity as t}from"@enkaku/token";import{applySyncMutations as s}from"./merkle-apply.js";import{buildMerkleTree as r}from"./merkle-tree.js";import{PeerRegistry as i}from"./peer-registry.js";import{SyncClient as n}from"./sync-client.js";export class SyncManager{#e;#t;#s;#r;#i;#n=new e;#o=new Map;#l=new Map;#a;#c;constructor(e){this.#e=e.db,this.#t=e.deployClusters,this.#s=e.runtime,this.#a=e.identity,this.#r=e.logger,this.#i=new i({db:e.db,runtime:e.runtime}),this.#c=e.serverResolver}setIdentity(e){this.#a=e}setServerResolver(e){this.#c=e}async addPeer(e){await this.#i.addPeer(e),this.#r.info("Peer added",{peerDID:e.peerDID})}async removePeer(e){await this.#i.removePeer(e),this.#r.info("Peer removed",{peerDID:e})}async updatePeerConfig(e,t){await this.#i.updatePeer(e,t),this.#r.info("Peer updated",{peerDID:e})}async listPeers(){return this.#i.listPeers()}async getPeer(e){return this.#i.getPeer(e)}async merkleSyncWithPeer(e,i,o=[],l){let a;this.#r.info("Starting Merkle sync with peer",{peerDID:e,scopes:i});let c=await this.#i.getPeer(e);if(!c)throw Error(`Peer ${e} not found`);let g=`merkle-sync-${e}-${Date.now()}`,m={peerID:e,startTime:Date.now(),documentsAttempted:0,documentsCompleted:0};this.#o.set(g,m),this.#g({type:"started",peerID:e,timestamp:Date.now()});try{let p=this.#a;if(!t(p))throw Error("Signing identity required for Merkle sync");let d=new n({runtime:this.#s,identity:p,logger:this.#r,serverResolver:this.#c});a=await d.connect(c.endpoint,e),this.#r.info("Negotiating sync scopes",{scopes:i});let{acceptedScopes:y,excludedDocumentIDs:h,missingClusters:u}=await d.negotiate(a,i,o,l?.knownModelIDs);if(0===y.length)return this.#r.info("No scopes accepted by peer"),this.#l.set(e,Date.now()),this.#g({type:"completed",peerID:e,timestamp:Date.now()}),this.#o.delete(g),{sessionID:g,divergentBuckets:0,messagesReceived:0,missingClusters:u};null!=u&&Object.keys(u).length>0&&(null!=this.#t?(this.#r.info("Deploying missing clusters from peer",{clusterCount:Object.keys(u).length}),await this.#t(u)):this.#r.warn("Missing clusters received but no deployClusters callback configured",{clusterCount:Object.keys(u).length})),this.#r.info("Building local Merkle tree",{acceptedScopes:y});let v=await this.#e.getDocumentIDsForScope(y,h),f=await this.#e.getMutationLogForDocuments(v),w=r(f);this.#r.info("Requesting Merkle sync from peer",{localTreeBuckets:Object.keys(w.buckets).length});let k=await d.merkleSync(a,{scopes:y,excludedDocumentIDs:h,tree:w.buckets});return k.mutationJWTs.length>0&&(this.#r.info("Applying sync mutations",{mutations:k.mutationJWTs.length}),m.documentsCompleted=(await s({db:this.#e,mutationJWTs:k.mutationJWTs})).applied),this.#l.set(e,Date.now()),this.#g({type:"completed",peerID:e,timestamp:Date.now()}),this.#r.info("Merkle sync completed",{divergentBuckets:k.divergentBuckets,mutationsReceived:k.mutationJWTs.length}),{sessionID:g,divergentBuckets:k.divergentBuckets,messagesReceived:k.mutationJWTs.length,missingClusters:u}}catch(t){throw this.#r.error("Merkle sync failed",{peerDID:e,error:t}),this.#g({type:"error",peerID:e,timestamp:Date.now(),error:t instanceof Error?t.message:String(t)}),t}finally{if(null!=a)try{a.abort("SyncComplete"),await a.dispose()}catch{}this.#o.delete(g)}}getStatus(e){let t=Array.from(this.#o.values()).filter(t=>!e||t.peerID===e).map(e=>({peerID:e.peerID,startTime:e.startTime,documentsAttempted:e.documentsAttempted,documentsCompleted:e.documentsCompleted})),s={};for(let[t,r]of this.#l.entries())e&&t!==e||(s[t]=r);return{activeSessions:t,lastSyncByPeer:s}}onSyncEvent(e){return this.#n.on("sync",e)}#g(e){this.#n.emit("sync",e).catch(e=>{this.#r.error("Error in sync event listener",{error:e})})}async dispose(){this.#o.clear(),this.#l.clear()}}
1
+ import{EventEmitter as e}from"@enkaku/event";import{isSigningIdentity as t}from"@enkaku/token";import{getGraphStore as r}from"@kubun/store-graph";import{applySyncMutations as s}from"./merkle-apply.js";import{buildMerkleTree as i}from"./merkle-tree.js";import{PeerRegistry as n}from"./peer-registry.js";import{SyncClient as o}from"./sync-client.js";export class SyncManager{#e;#t;#r;#s;#i=new e;#n=new Map;#o=new Map;#l;#a;constructor(e){this.#e=e.deployClusters,this.#t=e.runtime,this.#l=e.identity,this.#r=e.logger,this.#s=new n({runtime:e.runtime}),this.#a=e.serverResolver}get peerRegistry(){return this.#s}setIdentity(e){this.#l=e}setServerResolver(e){this.#a=e}async addPeer(e){await this.#s.addPeer(e),this.#r.info("Peer added",{peerDID:e.config.peerDID})}async removePeer(e){await this.#s.removePeer(e),this.#r.info("Peer removed",{peerDID:e.peerDID})}async updatePeerConfig(e){await this.#s.updatePeer(e),this.#r.info("Peer updated",{peerDID:e.peerDID})}async listPeers(e){return this.#s.listPeers(e)}async getPeer(e){return this.#s.getPeer(e)}async merkleSyncWithPeer(e){let n,{peerDID:l,scopes:a,delegationTokens:c=[],knownModelIDs:g,direction:m}=e;this.#r.info("Starting Merkle sync with peer",{peerDID:l,scopes:a});let p=await this.#s.getPeer({peerDID:l,stores:e.stores});if(!p)throw Error(`Peer ${l} not found`);let y=`merkle-sync-${l}-${Date.now()}`,u={peerID:l,startTime:Date.now(),documentsAttempted:0,documentsCompleted:0};this.#n.set(y,u),this.#c({type:"started",peerID:l,timestamp:Date.now()});try{let d=this.#l;if(!t(d))throw Error("Signing identity required for Merkle sync");let h=new o({runtime:this.#t,identity:d,logger:this.#r,serverResolver:this.#a});n=await h.connect(p.endpoint,l);let v=m??"pull";this.#r.info("Negotiating sync scopes",{scopes:a,direction:v});let{acceptedScopes:f,excludedDocumentIDs:D,missingClusters:w,direction:k}=await h.negotiate(n,a,c,g,v);if(0===f.length)return this.#r.info("No scopes accepted by peer"),this.#o.set(l,Date.now()),this.#c({type:"completed",peerID:l,timestamp:Date.now()}),this.#n.delete(y),{sessionID:y,divergentBuckets:0,messagesReceived:0,messagesSent:0,missingClusters:w};null!=w&&Object.keys(w).length>0&&(null!=this.#e?(this.#r.info("Deploying missing clusters from peer",{clusterCount:Object.keys(w).length}),await this.#e(w)):this.#r.warn("Missing clusters received but no deployClusters callback configured",{clusterCount:Object.keys(w).length})),this.#r.info("Building local Merkle tree",{acceptedScopes:f});let S=await r(e.stores),P=await S.getDocumentIDsForScope(f,D),R=await S.getMutationLogForDocuments(P),I=i(R),C=k??v;this.#r.info("Requesting Merkle sync from peer",{localTreeBuckets:Object.keys(I.buckets).length,direction:C});let B=await h.merkleSync(n,{scopes:f,excludedDocumentIDs:D,localTree:I,direction:C,localEntries:R});return B.mutationJWTs.length>0&&(this.#r.info("Applying sync mutations",{mutations:B.mutationJWTs.length}),u.documentsCompleted=(await s({db:e.stores,mutationJWTs:B.mutationJWTs})).applied),this.#o.set(l,Date.now()),this.#c({type:"completed",peerID:l,timestamp:Date.now()}),this.#r.info("Merkle sync completed",{divergentBuckets:B.divergentBuckets,mutationsReceived:B.mutationJWTs.length}),{sessionID:y,divergentBuckets:B.divergentBuckets,messagesReceived:B.mutationJWTs.length,messagesSent:B.mutationsSent,missingClusters:w}}catch(e){throw this.#r.error("Merkle sync failed",{peerDID:l,error:e}),this.#c({type:"error",peerID:l,timestamp:Date.now(),error:e instanceof Error?e.message:String(e)}),e}finally{if(null!=n)try{n.abort("SyncComplete"),await n.dispose()}catch{}this.#n.delete(y)}}getStatus(e){let{peerDID:t}=e,r=Array.from(this.#n.values()).filter(e=>!t||e.peerID===t).map(e=>({peerID:e.peerID,startTime:e.startTime,documentsAttempted:e.documentsAttempted,documentsCompleted:e.documentsCompleted})),s={};for(let[e,r]of this.#o.entries())t&&e!==t||(s[e]=r);return{activeSessions:r,lastSyncByPeer:s}}onSyncEvent(e){return this.#i.on("sync",e)}#c(e){this.#i.emit("sync",e).catch(e=>{this.#r.error("Error in sync event listener",{error:e})})}async dispose(){this.#n.clear(),this.#o.clear()}}
package/lib/types.d.ts CHANGED
@@ -1,24 +1,24 @@
1
1
  import type { ClientTransportOf } from '@enkaku/protocol';
2
- import type { SyncProtocol } from './protocol.js';
2
+ import type { SyncDirection, SyncProtocol } from './protocol.js';
3
3
  import type { PeerConfig, PeerConfigWithID } from './sync/peer-registry.js';
4
4
  import type { SyncScope } from './sync/sync-client.js';
5
5
  import type { SyncEvent, SyncStatus } from './sync/sync-manager.js';
6
- export type SpaceData = {
6
+ export type GroupData = {
7
7
  id: string;
8
8
  name: string;
9
9
  description: string;
10
10
  createdBy: string;
11
11
  createdAt: string;
12
12
  };
13
- export type SpaceMemberData = {
14
- spaceID: string;
13
+ export type GroupMemberData = {
14
+ groupID: string;
15
15
  memberDID: string;
16
16
  role: string;
17
17
  createdAt: string;
18
18
  };
19
19
  export type CircleData = {
20
20
  id: string;
21
- spaceID: string;
21
+ groupID: string;
22
22
  name: string;
23
23
  description: string;
24
24
  createdAt: string;
@@ -37,19 +37,19 @@ export type CatalogData = {
37
37
  filterCriteria: string;
38
38
  createdAt: string;
39
39
  };
40
- export type InviteToSpaceData = {
40
+ export type InviteToGroupData = {
41
41
  invitePayload: string;
42
- spaceID: string;
43
- spaceName: string;
42
+ groupID: string;
43
+ groupName: string;
44
44
  };
45
- export type JoinSpaceData = {
46
- space: SpaceData | null;
45
+ export type JoinGroupData = {
46
+ group: GroupData | null;
47
47
  };
48
- export type LeaveSpaceData = {
49
- spaceID: string;
48
+ export type LeaveGroupData = {
49
+ groupID: string;
50
50
  };
51
- export type RemoveSpaceMemberData = {
52
- spaceID: string;
51
+ export type RemoveGroupMemberData = {
52
+ groupID: string;
53
53
  };
54
54
  export type DeleteCatalogData = {
55
55
  deletedID: string;
@@ -61,8 +61,8 @@ export type RemoveCircleMemberData = {
61
61
  circleID: string;
62
62
  memberDID: string;
63
63
  };
64
- export type UpdateSpaceInput = {
65
- spaceID: string;
64
+ export type UpdateGroupInput = {
65
+ groupID: string;
66
66
  name?: string;
67
67
  description?: string;
68
68
  };
@@ -72,42 +72,42 @@ export type UpdateCircleInput = {
72
72
  description?: string;
73
73
  catalogIDs?: Array<string>;
74
74
  };
75
- export type SpaceRequestContext = {
76
- spaceCreateSpace: (input: {
75
+ export type GroupRequestContext = {
76
+ p2pCreateGroup: (input: {
77
77
  name: string;
78
78
  description?: string;
79
- }) => Promise<SpaceData>;
80
- spaceGetSpaces: () => Promise<Array<SpaceData>>;
81
- spaceGetSpace: (id: string) => Promise<SpaceData | null>;
82
- spaceAddSpaceMember: (spaceID: string, memberDID: string, role: string) => Promise<SpaceMemberData>;
83
- spaceGetSpaceMembers: (spaceID: string) => Promise<Array<SpaceMemberData>>;
84
- spaceCreateCircle: (params: {
85
- spaceID: string;
79
+ }) => Promise<GroupData>;
80
+ p2pGetGroups: () => Promise<Array<GroupData>>;
81
+ p2pGetGroup: (id: string) => Promise<GroupData | null>;
82
+ p2pAddGroupMember: (groupID: string, memberDID: string, role: string) => Promise<GroupMemberData>;
83
+ p2pGetGroupMembers: (groupID: string) => Promise<Array<GroupMemberData>>;
84
+ p2pCreateCircle: (params: {
85
+ groupID: string;
86
86
  name: string;
87
87
  description?: string;
88
88
  }) => Promise<CircleData>;
89
- spaceGetCircles: (spaceID: string) => Promise<Array<CircleData>>;
90
- spaceAddCircleMember: (circleID: string, memberDID: string, role: string) => Promise<CircleMemberData>;
91
- spaceCreateCatalog: (params: {
89
+ p2pGetCircles: (groupID: string) => Promise<Array<CircleData>>;
90
+ p2pAddCircleMember: (circleID: string, memberDID: string, role: string) => Promise<CircleMemberData>;
91
+ p2pCreateCatalog: (params: {
92
92
  name: string;
93
93
  description?: string;
94
94
  filterCriteria: string;
95
95
  }) => Promise<CatalogData>;
96
- spaceGetCatalogs: (ownerDID?: string) => Promise<Array<CatalogData>>;
97
- spaceInviteToSpace: (spaceID: string, joinRequest: string) => Promise<InviteToSpaceData>;
98
- spaceJoinSpace: (invitePayload: string, joinRequestPayload: string) => Promise<JoinSpaceData>;
99
- spaceLeaveSpace: (spaceID: string) => Promise<LeaveSpaceData>;
100
- spaceRemoveSpaceMember: (spaceID: string, memberDID: string) => Promise<RemoveSpaceMemberData>;
101
- spaceUpdateSpace: (input: UpdateSpaceInput) => Promise<SpaceData>;
102
- spaceUpdateCircle: (input: UpdateCircleInput) => Promise<CircleData>;
103
- spaceUpdateCatalog: (params: {
96
+ p2pGetCatalogs: (ownerDID?: string) => Promise<Array<CatalogData>>;
97
+ p2pInviteToGroup: (groupID: string, joinRequest: string) => Promise<InviteToGroupData>;
98
+ p2pJoinGroup: (invitePayload: string, joinRequestPayload: string) => Promise<JoinGroupData>;
99
+ p2pLeaveGroup: (groupID: string) => Promise<LeaveGroupData>;
100
+ p2pRemoveGroupMember: (groupID: string, memberDID: string) => Promise<RemoveGroupMemberData>;
101
+ p2pUpdateGroup: (input: UpdateGroupInput) => Promise<GroupData>;
102
+ p2pUpdateCircle: (input: UpdateCircleInput) => Promise<CircleData>;
103
+ p2pUpdateCatalog: (params: {
104
104
  catalogID: string;
105
105
  name?: string;
106
106
  filterCriteria?: string;
107
107
  }) => Promise<CatalogData>;
108
- spaceDeleteCatalog: (catalogID: string) => Promise<DeleteCatalogData>;
109
- spaceDeleteCircle: (circleID: string) => Promise<DeleteCircleData>;
110
- spaceRemoveCircleMember: (circleID: string, memberDID: string) => Promise<RemoveCircleMemberData>;
108
+ p2pDeleteCatalog: (catalogID: string) => Promise<DeleteCatalogData>;
109
+ p2pDeleteCircle: (circleID: string) => Promise<DeleteCircleData>;
110
+ p2pRemoveCircleMember: (circleID: string, memberDID: string) => Promise<RemoveCircleMemberData>;
111
111
  };
112
112
  export type SyncRequestContext = {
113
113
  syncAddPeer: (config: PeerConfig) => Promise<void>;
@@ -120,13 +120,14 @@ export type P2PJoinRequestContext = {
120
120
  joinRequest: string;
121
121
  }>;
122
122
  p2pCompleteJoin: (invitePayload: string) => Promise<{
123
- space: SpaceData | null;
123
+ group: GroupData | null;
124
124
  }>;
125
125
  };
126
126
  export type SyncSessionResult = {
127
127
  sessionID: string;
128
128
  divergentBuckets: number;
129
129
  messagesReceived: number;
130
+ messagesSent: number;
130
131
  missingClusters?: Record<string, unknown>;
131
132
  };
132
133
  export type SyncPluginAPI = {
@@ -140,6 +141,7 @@ export type SyncPluginAPI = {
140
141
  scopes: Array<SyncScope>;
141
142
  delegationTokens?: Array<string>;
142
143
  knownModelIDs?: Array<string>;
144
+ direction?: SyncDirection;
143
145
  }): Promise<SyncSessionResult>;
144
146
  getStatus(peerDID?: string): SyncStatus;
145
147
  onSyncEvent(callback: (event: SyncEvent) => void): () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/plugin-p2p",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "license": "see LICENSE.md",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -14,42 +14,46 @@
14
14
  ],
15
15
  "sideEffects": false,
16
16
  "dependencies": {
17
- "@enkaku/async": "^0.14.0",
17
+ "@enkaku/async": "^0.14.1",
18
18
  "@enkaku/capability": "^0.14.0",
19
- "@enkaku/client": "^0.14.3",
19
+ "@enkaku/client": "^0.14.4",
20
20
  "@enkaku/codec": "^0.14.0",
21
21
  "@enkaku/event": "^0.14.1",
22
22
  "@enkaku/generator": "^0.14.0",
23
23
  "@enkaku/group": "^0.14.6",
24
24
  "@enkaku/http-client-transport": "^0.14.3",
25
25
  "@enkaku/http-server-transport": "^0.14.2",
26
+ "@enkaku/hub-protocol": "^0.14.0",
26
27
  "@enkaku/protocol": "^0.14.0",
27
28
  "@enkaku/runtime": "^0.14.0",
28
29
  "@enkaku/schema": "^0.14.0",
29
- "@enkaku/server": "^0.14.3",
30
+ "@enkaku/server": "^0.14.4",
30
31
  "@enkaku/stream": "^0.14.1",
31
32
  "@enkaku/token": "0.14.1",
32
- "@enkaku/transport": "0.14.0",
33
+ "@enkaku/transport": "0.14.1",
33
34
  "@noble/hashes": "^2.2.0",
34
35
  "graphql": "^16.13.2",
35
36
  "kysely": "^0.28.16",
36
37
  "@kubun/client": "^0.8.0",
37
- "@kubun/graphql": "^0.8.0",
38
+ "@kubun/store-p2p": "^0.8.0",
39
+ "@kubun/graphql": "^0.8.1",
40
+ "@kubun/engine": "^0.8.2",
41
+ "@kubun/id": "^0.8.0",
38
42
  "@kubun/hlc": "^0.8.0",
39
- "@kubun/db-adapter": "^0.8.0",
40
- "@kubun/db": "^0.8.0",
41
- "@kubun/engine": "^0.8.0",
43
+ "@kubun/mutation": "^0.8.1",
44
+ "@kubun/db": "^0.8.1",
45
+ "@kubun/store-graph": "^0.8.1",
46
+ "@kubun/protocol": "^0.8.1",
42
47
  "@kubun/logger": "^0.8.0",
43
- "@kubun/id": "^0.8.0",
44
- "@kubun/mutation": "^0.8.0",
45
- "@kubun/protocol": "^0.8.0"
48
+ "@kubun/db-adapter": "^0.8.1"
46
49
  },
47
50
  "devDependencies": {
51
+ "@enkaku/hub-server": "^0.14.1",
48
52
  "get-port": "^7.2.0",
49
- "@kubun/db-better-sqlite": "^0.8.0",
50
- "@kubun/plugin-connector": "^0.8.0",
51
- "@kubun/plugin-http": "^0.8.0",
52
- "@kubun/plugin-rpc": "^0.8.0",
53
+ "@kubun/db-better-sqlite": "^0.8.1",
54
+ "@kubun/plugin-connector": "^0.8.1",
55
+ "@kubun/plugin-rpc": "^0.8.1",
56
+ "@kubun/plugin-http": "^0.8.1",
53
57
  "@kubun/test-utils": "^0.8.0"
54
58
  },
55
59
  "scripts": {
@@ -1,4 +0,0 @@
1
- import type { ExecutionContext } from '@kubun/engine';
2
- import type { SpaceRequestContext } from '../types.js';
3
- import type { ContextDeps } from './types.js';
4
- export declare function createSpaceContext(ctx: ExecutionContext, deps: ContextDeps): SpaceRequestContext;
@@ -1 +0,0 @@
1
- import{HLC as e}from"@kubun/hlc";import{restoreGroupHandle as t}from"../spaces/group-handle.js";import{decodeFullJoinRequest as a,decodeInvitePayload as r,decodeJoinRequest as i,encodeInvitePayload as c}from"../spaces/invite-payload.js";import{createSpaceFromJoin as l}from"../spaces/join-utils.js";import{checkPeerAccess as s,normalizeCredential as d,toISO as n}from"./types.js";export function createSpaceContext(p,o){return{spaceCreateSpace:async e=>await p.transaction(async t=>{let a=t.db,r=await o.spaceManager.createSpace({identity:o.identity,name:e.name,description:e.description},a);await t.db.updateTable("kubun_spaces").set({created_by:p.viewerDID}).where("id","=",r.spaceID).execute();let i=await t.db.selectFrom("kubun_spaces").selectAll().where("id","=",r.spaceID).executeTakeFirst();if(null==i)throw Error("Failed to create space");t.emit("space:created",{spaceID:r.spaceID});let c={id:i.id,name:i.name,description:i.description,createdBy:i.created_by,createdAt:n(i.created_at)};return await o.emitter.emit("spaceJoined",c),c}),spaceGetSpaces:async()=>{let e=await o.getDB();return(await e.selectFrom("kubun_spaces").selectAll().execute()).map(e=>({id:e.id,name:e.name,description:e.description,createdBy:e.created_by,createdAt:n(e.created_at)}))},spaceGetSpace:async e=>{let t=await o.getDB(),a=await t.selectFrom("kubun_spaces").selectAll().where("id","=",e).executeTakeFirst();return null==a?null:{id:a.id,name:a.name,description:a.description,createdBy:a.created_by,createdAt:n(a.created_at)}},spaceAddSpaceMember:async(e,t,a)=>{let r=await o.getDB();await r.insertInto("kubun_space_members").values({space_id:e,member_did:t,role:a}).onConflict(e=>e.columns(["space_id","member_did"]).doUpdateSet(e=>({role:e.ref("excluded.role"),updated_at:o.adapter.encodeTimestamp(new Date)}))).execute();let i=await r.selectFrom("kubun_space_members").selectAll().where("space_id","=",e).where("member_did","=",t).executeTakeFirst();if(null==i)throw Error("Failed to add space member");let c={spaceID:i.space_id,memberDID:i.member_did,role:i.role,createdAt:n(i.created_at)};return await o.emitter.emit("spaceMemberJoined",{...c,spaceID:c.spaceID}),c},spaceGetSpaceMembers:async e=>{let t=await o.getDB();return(await t.selectFrom("kubun_space_members").selectAll().where("space_id","=",e).execute()).map(e=>({spaceID:e.space_id,memberDID:e.member_did,role:e.role,createdAt:n(e.created_at)}))},spaceCreateCircle:async e=>{let t=await o.spaceManager.createCircle({spaceID:e.spaceID,name:e.name,description:e.description}),a=await o.getDB(),r=await a.selectFrom("kubun_circles").selectAll().where("id","=",t.circleID).executeTakeFirst();if(null==r)throw Error("Failed to create circle");let i={id:r.id,spaceID:r.space_id,name:r.name,description:r.description,createdAt:n(r.created_at)};return await o.emitter.emit("circleCreated",{...i,spaceID:i.spaceID}),i},spaceGetCircles:async e=>{let t=await o.getDB();return(await t.selectFrom("kubun_circles").selectAll().where("space_id","=",e).execute()).map(e=>({id:e.id,spaceID:e.space_id,name:e.name,description:e.description,createdAt:n(e.created_at)}))},spaceAddCircleMember:async(t,a,r)=>{let i=await o.getDB(),c=e.serialize(o.hlc.now());await i.insertInto("kubun_circle_members").values({circle_id:t,member_did:a,role:r,hlc:c}).onConflict(e=>e.columns(["circle_id","member_did"]).doUpdateSet(e=>({role:e.ref("excluded.role"),hlc:e.ref("excluded.hlc")}))).execute();let l=await i.selectFrom("kubun_circle_members").selectAll().where("circle_id","=",t).where("member_did","=",a).executeTakeFirst();if(null==l)throw Error("Failed to add circle member");let s={circleID:l.circle_id,memberDID:l.member_did,role:l.role,createdAt:n(l.created_at)};return await o.emitter.emit("circleMemberAdded",{...s,circleID:s.circleID}),s},spaceCreateCatalog:async t=>{let a=await o.getDB(),r=o.runtime.getRandomID(),i=e.serialize(o.hlc.now());await a.insertInto("kubun_catalogs").values({id:r,owner_did:p.viewerDID,name:t.name,description:t.description??"",filter_criteria:o.adapter.encodeJSON("string"==typeof t.filterCriteria?JSON.parse(t.filterCriteria):t.filterCriteria),hlc:i}).execute();let c=await a.selectFrom("kubun_catalogs").selectAll().where("id","=",r).executeTakeFirst();if(null==c)throw Error("Failed to create catalog");return{id:c.id,ownerDID:c.owner_did,name:c.name,description:c.description,filterCriteria:"string"==typeof c.filter_criteria?c.filter_criteria:JSON.stringify(c.filter_criteria),createdAt:n(c.created_at)}},spaceGetCatalogs:async e=>{let t=(await o.getDB()).selectFrom("kubun_catalogs").selectAll();return null!=e&&(t=t.where("owner_did","=",e)),(await t.execute()).map(e=>({id:e.id,ownerDID:e.owner_did,name:e.name,description:e.description,filterCriteria:"string"==typeof e.filter_criteria?e.filter_criteria:JSON.stringify(e.filter_criteria),createdAt:n(e.created_at)}))},spaceInviteToSpace:async(e,a)=>{s(p.viewerDID,o.identity.id,o.autoAcceptPeers);let r=await o.getDB(),l=o.identity,n=await r.selectFrom("kubun_space_mls_state").selectAll().where("space_id","=",e).where("device_id","=",l.id).executeTakeFirst();if(null==n)throw Error(`No MLS state for space ${e}`);let m=await t({mlsState:n.mls_state,credential:d(n.credential),epoch:n.epoch,rootCapability:n.root_capability}),u=i(a),w=await o.spaceManager.inviteToSpace({spaceID:e,identity:l,groupHandle:m,recipientDID:u.did,recipientKeyPackage:u.publicPackage,permission:"member"},r),_=await r.selectFrom("kubun_spaces").select(["name","hub_urls"]).where("id","=",e).executeTakeFirst(),D="string"==typeof _?.hub_urls?JSON.parse(_.hub_urls):_?.hub_urls??[],b=c({spaceID:e,spaceName:_?.name??"",hubURLs:D,invite:w.invite,welcomeMessage:w.welcomeMessage,ratchetTree:w.updatedGroupHandle.state.ratchetTree});return await o.emitter.emit("spaceMemberJoined",{spaceID:e,memberDID:u.did,role:"member",createdAt:new Date().toISOString()}),{invitePayload:b,spaceID:e,spaceName:_?.name??""}},spaceJoinSpace:async(e,t)=>{let i=await o.getDB(),c=o.identity,s=r(e),d=a(t),n={publicPackage:d.publicPackage,privatePackage:d.privatePackage,ownerDID:d.did};return await o.spaceManager.joinSpace({identity:c,spaceID:s.spaceID,invite:s.invite,keyPackageBundle:n,welcomeMessage:s.welcomeMessage,ratchetTree:s.ratchetTree},i),await l({db:i,adapter:o.adapter,hlc:o.hlc,emitter:o.emitter,identity:c,invite:s})},spaceLeaveSpace:async e=>{let t=await o.getDB(),a=await t.selectFrom("kubun_spaces").selectAll().where("id","=",e).executeTakeFirst(),r=o.identity;return await o.spaceManager.leaveSpace({spaceID:e,identity:r}),null!=a&&await o.emitter.emit("spaceLeft",{id:a.id,name:a.name,description:a.description,createdBy:a.created_by,createdAt:n(a.created_at),spaceID:e}),{spaceID:e}},spaceRemoveSpaceMember:async(e,a)=>{let r=await o.getDB(),i=o.identity,c=await r.selectFrom("kubun_space_mls_state").selectAll().where("space_id","=",e).where("device_id","=",i.id).executeTakeFirst();if(null==c)throw Error(`No MLS state for space ${e}`);let l=await t({mlsState:c.mls_state,credential:d(c.credential),epoch:c.epoch,rootCapability:c.root_capability});return await o.spaceManager.removeSpaceMember({spaceID:e,groupHandle:l,memberDID:a},r),await o.emitter.emit("spaceMemberLeft",{spaceID:e,memberDID:a,role:"",createdAt:new Date().toISOString()}),{spaceID:e}},spaceUpdateSpace:async e=>{await o.spaceManager.updateSpace({spaceID:e.spaceID,update:{name:e.name,description:e.description}});let t=await o.getDB(),a=await t.selectFrom("kubun_spaces").selectAll().where("id","=",e.spaceID).executeTakeFirst();if(null==a)throw Error(`Space ${e.spaceID} not found`);let r={id:a.id,name:a.name,description:a.description,createdBy:a.created_by,createdAt:n(a.created_at)};return await o.emitter.emit("spaceDataChanged",{...r,spaceID:e.spaceID}),r},spaceUpdateCircle:async e=>{await o.spaceManager.updateCircle({circleID:e.circleID,update:{name:e.name,description:e.description,catalogIDs:e.catalogIDs}});let t=await o.getDB(),a=await t.selectFrom("kubun_circles").selectAll().where("id","=",e.circleID).executeTakeFirst();if(null==a)throw Error(`Circle ${e.circleID} not found`);let r={id:a.id,spaceID:a.space_id,name:a.name,description:a.description,createdAt:n(a.created_at)};return null!=e.catalogIDs?await o.emitter.emit("circleCatalogsChanged",{...r,circleID:e.circleID}):await o.emitter.emit("circleDataChanged",{...r,circleID:e.circleID}),r},spaceUpdateCatalog:async t=>{let a=await o.getDB(),r={hlc:e.serialize(o.hlc.now())};null!=t.name&&(r.name=t.name),null!=t.filterCriteria&&(r.filter_criteria=o.adapter.encodeJSON("string"==typeof t.filterCriteria?JSON.parse(t.filterCriteria):t.filterCriteria)),r.updated_at=o.adapter.encodeTimestamp(new Date),await a.updateTable("kubun_catalogs").set(r).where("id","=",t.catalogID).execute();let i=await a.selectFrom("kubun_catalogs").selectAll().where("id","=",t.catalogID).executeTakeFirst();if(null==i)throw Error(`Catalog ${t.catalogID} not found`);return{id:i.id,ownerDID:i.owner_did,name:i.name,description:i.description,filterCriteria:"string"==typeof i.filter_criteria?i.filter_criteria:JSON.stringify(i.filter_criteria),createdAt:n(i.created_at)}},spaceDeleteCatalog:async e=>{let t=await o.getDB();return await t.deleteFrom("kubun_catalogs").where("id","=",e).execute(),{deletedID:e}},spaceDeleteCircle:async e=>{let t=await o.getDB(),a=await t.selectFrom("kubun_circles").selectAll().where("id","=",e).executeTakeFirst();return await o.spaceManager.deleteCircle({circleID:e}),null!=a&&await o.emitter.emit("circleDeleted",{id:a.id,spaceID:a.space_id,name:a.name,description:a.description,createdAt:n(a.created_at)}),{deletedID:e}},spaceRemoveCircleMember:async(e,t)=>(await o.spaceManager.removeCircleMember({circleID:e,memberDID:t}),await o.emitter.emit("circleMemberRemoved",{circleID:e,memberDID:t,role:"",createdAt:new Date().toISOString()}),{circleID:e,memberDID:t})}}
@@ -1,8 +0,0 @@
1
- import type { Adapter } from '@kubun/db-adapter';
2
- import type { PluginMigrationContext } from '@kubun/engine';
3
- import type { Migration } from 'kysely';
4
- export type MigrationContext = {
5
- types: Adapter['types'];
6
- functions: Adapter['functions'];
7
- };
8
- export declare function createP2PMigrations(ctx: MigrationContext | PluginMigrationContext): Record<string, Migration>;
package/lib/migrations.js DELETED
@@ -1 +0,0 @@
1
- export function createP2PMigrations(e){let t=e.types,a=e.functions;return{"001_create_p2p_tables":{async up(e){await e.schema.createTable("kubun_spaces").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("name",t.text,e=>e.notNull()).addColumn("description",t.text,e=>e.notNull().defaultTo("")).addColumn("created_by",t.text,e=>e.notNull()).addColumn("hub_urls",t.json,e=>e.defaultTo("[]").notNull()).addColumn("hlc",t.text,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a.now).notNull()).addColumn("updated_at",t.timestamp).execute(),await e.schema.createTable("kubun_space_members").ifNotExists().addColumn("space_id",t.text,e=>e.notNull()).addColumn("member_did",t.text,e=>e.notNull()).addColumn("role",t.text,e=>e.notNull().defaultTo("member")).addColumn("created_at",t.timestamp,e=>e.defaultTo(a.now).notNull()).addColumn("updated_at",t.timestamp).addPrimaryKeyConstraint("pk_space_members",["space_id","member_did"]).execute(),await e.schema.createIndex("idx_space_members_did").ifNotExists().on("kubun_space_members").column("member_did").execute(),await e.schema.createTable("kubun_circles").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("space_id",t.text,e=>e.notNull()).addColumn("name",t.text,e=>e.notNull()).addColumn("description",t.text,e=>e.notNull().defaultTo("")).addColumn("catalog_ids",t.json,e=>e.notNull().defaultTo("[]")).addColumn("hlc",t.text,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a.now).notNull()).addColumn("updated_at",t.timestamp).execute(),await e.schema.createIndex("idx_circles_space").ifNotExists().on("kubun_circles").column("space_id").execute(),await e.schema.createTable("kubun_circle_members").ifNotExists().addColumn("circle_id",t.text,e=>e.notNull()).addColumn("member_did",t.text,e=>e.notNull()).addColumn("role",t.text,e=>e.notNull().defaultTo("member")).addColumn("hlc",t.text,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a.now).notNull()).addColumn("updated_at",t.timestamp).addPrimaryKeyConstraint("pk_circle_members",["circle_id","member_did"]).execute(),await e.schema.createIndex("idx_circle_members_did").ifNotExists().on("kubun_circle_members").column("member_did").execute(),await e.schema.createTable("kubun_catalogs").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("owner_did",t.text,e=>e.notNull()).addColumn("name",t.text,e=>e.notNull()).addColumn("description",t.text,e=>e.notNull().defaultTo("")).addColumn("filter_criteria",t.json,e=>e.notNull()).addColumn("hlc",t.text,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a.now).notNull()).addColumn("updated_at",t.timestamp).execute(),await e.schema.createIndex("idx_catalogs_owner").ifNotExists().on("kubun_catalogs").column("owner_did").execute(),await e.schema.createTable("kubun_space_mls_state").ifNotExists().addColumn("space_id",t.text,e=>e.notNull()).addColumn("device_id",t.text,e=>e.notNull()).addColumn("mls_state",t.binary,e=>e.notNull()).addColumn("credential",t.text,e=>e.notNull()).addColumn("epoch","integer",e=>e.notNull()).addColumn("root_capability",t.text,e=>e.notNull().defaultTo("")).addColumn("updated_at",t.timestamp,e=>e.defaultTo(a.now).notNull()).addPrimaryKeyConstraint("pk_space_mls_state",["space_id","device_id"]).execute(),await e.schema.createTable("kubun_sync_peers").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("peer_did",t.text,e=>e.notNull().unique()).addColumn("endpoint",t.text,e=>e.notNull()).addColumn("mode",t.text,e=>e.notNull()).addColumn("allowed_users",t.json,e=>e.notNull()).addColumn("priority",t.text,e=>e.notNull().defaultTo("0")).addColumn("trust_level",t.text,e=>e.notNull()).addColumn("config",t.json,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a.now).notNull()).addColumn("updated_at",t.timestamp,e=>e.defaultTo(a.now).notNull()).execute(),await e.schema.createTable("kubun_sync_checkpoints").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("peer_did",t.text,e=>e.notNull()).addColumn("scope",t.text,e=>e.notNull()).addColumn("checkpoint_hlc",t.text,e=>e.notNull()).addColumn("updated_at",t.timestamp,e=>e.defaultTo(a.now).notNull()).execute()},async down(e){await e.schema.dropTable("kubun_sync_checkpoints").ifExists().execute(),await e.schema.dropTable("kubun_sync_peers").ifExists().execute(),await e.schema.dropTable("kubun_space_mls_state").ifExists().execute(),await e.schema.dropTable("kubun_catalogs").ifExists().execute(),await e.schema.dropTable("kubun_circle_members").ifExists().execute(),await e.schema.dropTable("kubun_circles").ifExists().execute(),await e.schema.dropTable("kubun_space_members").ifExists().execute(),await e.schema.dropTable("kubun_spaces").ifExists().execute()}}}}
@@ -1,3 +0,0 @@
1
- import type { SpaceBroadcastMessage } from './broadcast.js';
2
- export declare function serializeBroadcast(message: SpaceBroadcastMessage): Uint8Array;
3
- export declare function deserializeBroadcast(bytes: Uint8Array): SpaceBroadcastMessage;
@@ -1,59 +0,0 @@
1
- import { EventEmitter } from '@enkaku/event';
2
- import type { Database } from '@kubun/db';
3
- import type { AbstractAdapter, AdapterTypes } from '@kubun/db-adapter';
4
- import type { Kysely } from 'kysely';
5
- import { type SpaceBroadcastMessage } from './broadcast.js';
6
- import { type SerializedGroupState } from './mls-state.js';
7
- export type BroadcastEvent = {
8
- spaceID: string;
9
- message: SpaceBroadcastMessage;
10
- applied: boolean;
11
- };
12
- export type BroadcastServiceEvents = {
13
- broadcast: BroadcastEvent;
14
- };
15
- export type SendBroadcastParams = {
16
- spaceID: string;
17
- message: SpaceBroadcastMessage;
18
- groupState: SerializedGroupState;
19
- };
20
- export type SendBroadcastResult = {
21
- encrypted: Uint8Array;
22
- updatedGroupState: SerializedGroupState;
23
- };
24
- export type ReceiveBroadcastParams = {
25
- spaceID: string;
26
- encrypted: Uint8Array;
27
- groupState: SerializedGroupState;
28
- };
29
- export type ReceiveBroadcastResult = {
30
- message: SpaceBroadcastMessage;
31
- applied: boolean;
32
- updatedGroupState: SerializedGroupState;
33
- };
34
- /**
35
- * BroadcastService handles MLS encrypt/decrypt for space broadcasts
36
- * and applies received messages to the local DB.
37
- *
38
- * It does NOT manage hub transport directly -- callers are responsible
39
- * for sending encrypted bytes via hub and delivering received bytes
40
- * from hub. This keeps the service testable without hub infrastructure.
41
- */
42
- export type BroadcastServiceParams<T extends AdapterTypes = AdapterTypes> = {
43
- kysely: Kysely<Database>;
44
- adapter: AbstractAdapter<T>;
45
- };
46
- export declare class BroadcastService extends EventEmitter<BroadcastServiceEvents> {
47
- #private;
48
- constructor(params: BroadcastServiceParams);
49
- /**
50
- * Prepare a broadcast for sending: serialize + MLS encrypt.
51
- * Returns encrypted bytes to send via hub and updated MLS state to persist.
52
- */
53
- prepareSend(params: SendBroadcastParams): Promise<SendBroadcastResult>;
54
- /**
55
- * Process a received broadcast: MLS decrypt + deserialize + apply to DB.
56
- * Returns the decoded message, whether it was applied, and updated MLS state.
57
- */
58
- processReceived(params: ReceiveBroadcastParams): Promise<ReceiveBroadcastResult>;
59
- }