@kubun/server 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
+ import type { Tracer } from '@enkaku/otel';
1
2
  import type { KubunDB, WritableDB } from '@kubun/db';
2
- import { type Context } from '@kubun/graphql';
3
+ import { type AccessWarning, type ComplexityStats, type Context } from '@kubun/graphql';
3
4
  import { type MutationOperations } from '@kubun/mutation';
4
5
  import type { DocumentNode } from '@kubun/protocol';
5
6
  import { type ExecutionArgs, type GraphQLSchema, type OperationTypeNode } from 'graphql';
@@ -13,6 +14,10 @@ export type ExecutionContext = {
13
14
  mutationOperations?: MutationOperations<DocumentNode>;
14
15
  accessChecker?: AccessChecker;
15
16
  transactionManager?: TransactionManager;
17
+ warnings?: Array<AccessWarning>;
18
+ complexity?: ComplexityStats;
19
+ debug?: boolean;
20
+ tracer?: Tracer;
16
21
  };
17
22
  export declare function createContext(ctx: ExecutionContext): Context;
18
23
  export type ExecuteGraphQLParams = {
@@ -1 +1 @@
1
- import{createReadContext as t}from"@kubun/graphql";import{convertPatchInput as a}from"@kubun/mutation";import{Kind as e,parse as n}from"graphql";import{applyBufferedMutations as r,applyMutationsAtomically as o}from"./apply-atomic.js";import{removeDocumentAccessOverride as i,removeModelAccessDefaults as c,setDocumentAccessOverride as s,setModelAccessDefaults as u}from"./mutations.js";export function createContext(e){let n=t({db:e.db,viewerDID:e.viewerDID,accessChecker:e.accessChecker}),l={executeSetModelAccessDefaults:async t=>await u({ownerDID:e.viewerDID,...t},e.db),async executeRemoveModelAccessDefaults(t,a){await c({ownerDID:e.viewerDID,modelID:t,permissionTypes:a},e.db)},executeSetDocumentAccessOverride:async t=>await s(t,e.db),async executeRemoveDocumentAccessOverride(t,a){await i({documentID:t,permissionTypes:a},e.db)}},m={beginTransaction(){if(null==e.transactionManager)throw Error("Transactions are not available");return{transactionID:e.transactionManager.beginTransaction().id}},async commitTransaction(t){if(null==e.transactionManager)throw Error("Transactions are not available");let a=e.transactionManager.getTransaction(t);if(null==a)throw Error("Transaction not found or expired");return await o({db:e.db,mutations:a.mutations}),e.transactionManager.markCommitted(t),{success:!0}},rollbackTransaction(t){if(null==e.transactionManager)throw Error("Transactions are not available");return e.transactionManager.rollbackTransaction(t),{success:!0}}};if(null!=e.mutationOperations){let t=e.mutationOperations;return{...n,executeCreateMutation:async({modelID:a,data:e,transactionID:n})=>await t.createDocument({modelID:a,data:e,transactionID:n}),executeSetMutation:async({modelID:a,unique:e,data:n,transactionID:r})=>await t.setDocument({modelID:a,unique:e,data:n,transactionID:r}),executeUpdateMutation:async({input:e,transactionID:n})=>await t.updateDocument({docID:e.id,patch:a(e.patch),transactionID:n}),async executeRemoveMutation({id:a,transactionID:e}){await t.removeDocument({docID:a,transactionID:e})},...l,...m,async commitTransaction(t){if(null==e.transactionManager)throw Error("Transactions are not available");let a=e.transactionManager.getTransaction(t);if(null==a)throw Error("Transaction not found or expired");return await r(e.transactionalDB??e.db,a.mutations),e.transactionManager.markCommitted(t),{success:!0}}}}function d(t){return e.mutatedDocuments?.[t.path.key]}return{...n,executeCreateMutation:async({info:t})=>d(t),executeSetMutation:async({info:t})=>d(t),executeUpdateMutation:async({info:t})=>d(t),async executeRemoveMutation(){},...l,...m}}export function getExecutionArgs(t){let a=n(t.text),r=a.definitions[0];if(null==r)throw Error("Missing GraphQL document definition");if(r.kind!==e.OPERATION_DEFINITION||r.operation!==t.type)throw Error(`Invalid GraphQL document definition: expected ${t.type} operation`);return{document:a,schema:t.schema,variableValues:t.variables,contextValue:createContext(t.context)}}
1
+ import{withSpan as t}from"@enkaku/otel";import{createReadContext as a}from"@kubun/graphql";import{convertPatchInput as e}from"@kubun/mutation";import{KubunAttributeKeys as r,KubunSpanNames as n}from"@kubun/protocol";import{Kind as o,parse as i}from"graphql";import{applyBufferedMutations as c,applyMutationsAtomically as s}from"./apply-atomic.js";import{removeDocumentAccessOverride as u,removeModelAccessDefaults as l,setDocumentAccessOverride as m,setModelAccessDefaults as T}from"./mutations.js";export function createContext(o){let i=a({db:o.db,viewerDID:o.viewerDID,accessChecker:o.accessChecker,warnings:o.warnings,complexity:o.complexity,tracer:o.tracer}),D={executeSetModelAccessDefaults:async t=>await T({ownerDID:o.viewerDID,...t},o.db),async executeRemoveModelAccessDefaults(t,a){await l({ownerDID:o.viewerDID,modelID:t,permissionTypes:a},o.db)},executeSetDocumentAccessOverride:async t=>await m(t,o.db),async executeRemoveDocumentAccessOverride(t,a){await u({documentID:t,permissionTypes:a},o.db)}},d={beginTransaction(){if(null==o.transactionManager)throw Error("Transactions are not available");return{transactionID:o.transactionManager.beginTransaction().id}},async commitTransaction(t){if(null==o.transactionManager)throw Error("Transactions are not available");let a=o.transactionManager.getTransaction(t);if(null==a)throw Error("Transaction not found or expired");return await s({db:o.db,mutations:a.mutations}),o.transactionManager.markCommitted(t),{success:!0}},rollbackTransaction(t){if(null==o.transactionManager)throw Error("Transactions are not available");return o.transactionManager.rollbackTransaction(t),{success:!0}}};if(null!=o.mutationOperations){let a=o.mutationOperations;return{...i,async executeCreateMutation({modelID:e,data:i,transactionID:c}){let s=async()=>await a.createDocument({modelID:e,data:i,transactionID:c});return null!=o.tracer?await t(o.tracer,n.DATA_MUTATE,{attributes:{[r.MODEL_ID]:e,[r.MUTATION_TYPE]:"create"}},s):await s()},async executeSetMutation({modelID:e,unique:i,data:c,transactionID:s}){let u=async()=>await a.setDocument({modelID:e,unique:i,data:c,transactionID:s});return null!=o.tracer?await t(o.tracer,n.DATA_MUTATE,{attributes:{[r.MODEL_ID]:e,[r.MUTATION_TYPE]:"set"}},u):await u()},async executeUpdateMutation({input:i,transactionID:c}){let s=async()=>await a.updateDocument({docID:i.id,patch:e(i.patch),transactionID:c});return null!=o.tracer?await t(o.tracer,n.DATA_MUTATE,{attributes:{[r.DOCUMENT_ID]:i.id,[r.MUTATION_TYPE]:"update"}},s):await s()},async executeRemoveMutation({id:e,transactionID:i}){let c=async()=>{await a.removeDocument({docID:e,transactionID:i})};null!=o.tracer?await t(o.tracer,n.DATA_MUTATE,{attributes:{[r.DOCUMENT_ID]:e,[r.MUTATION_TYPE]:"remove"}},c):await c()},...D,...d,async commitTransaction(t){if(null==o.transactionManager)throw Error("Transactions are not available");let a=o.transactionManager.getTransaction(t);if(null==a)throw Error("Transaction not found or expired");return await c(o.transactionalDB??o.db,a.mutations),o.transactionManager.markCommitted(t),{success:!0}}}}function p(t){return o.mutatedDocuments?.[t.path.key]}return{...i,executeCreateMutation:async({info:t})=>p(t),executeSetMutation:async({info:t})=>p(t),executeUpdateMutation:async({info:t})=>p(t),async executeRemoveMutation(){},...D,...d}}export function getExecutionArgs(t){let a=i(t.text),e=a.definitions[0];if(null==e)throw Error("Missing GraphQL document definition");if(e.kind!==o.OPERATION_DEFINITION||e.operation!==t.type)throw Error(`Invalid GraphQL document definition: expected ${t.type} operation`);return{document:a,schema:t.schema,variableValues:t.variables,contextValue:createContext(t.context)}}
@@ -1 +1 @@
1
- import{fromB64 as a,toB64 as t}from"@enkaku/codec";import{consume as e}from"@enkaku/generator";import{stringifyToken as r}from"@enkaku/token";import{createSchema as n}from"@kubun/graphql";import{AttachmentID as o,DocumentID as i}from"@kubun/id";import{applyChangeMutation as s,applySetMutation as l,createMutationOperations as c,HLC as d}from"@kubun/mutation";import{GraphModel as u}from"@kubun/protocol";import{execute as m,OperationTypeNode as p,subscribe as h}from"graphql";import{createAccessChecker as f}from"../data/access-control.js";import{applyMutationsAtomically as w}from"../data/apply-atomic.js";import{getExecutionArgs as g}from"../data/graphql.js";import{captureMutation as b}from"../data/mutation-capture.js";import{applyMutation as y}from"../data/mutations.js";function D(a){let t={data:a.data};return null!=a.errors&&(t.errors=a.errors.map(a=>a.toJSON())),null!=a.extensions&&(t.extensions=a.extensions),t}export function createHandlers(x){let{db:v,logger:I,serverAccessConfig:k,signingIdentity:T,transactionManager:S}=x,A=new d({nodeID:"server"}),j={};function E(a){return a?.fieldsMeta?Object.entries(a.fieldsMeta).filter(([a,t])=>!0===t.searchable).map(([a])=>a):[]}async function O(a,t,e){let r;I.info("starting backfill for model {modelID} in graph {graphID}",{modelID:t,graphID:a});let n=0,o=!0;for(;o;){let a=await v.queryDocuments({modelIDs:[t],first:100,after:r});for(let r of a.entries)r.document.data&&(await v.updateSearchEntry(t,r.document.id,r.document.data,e),n++);o=a.hasMore,r=a.entries.at(-1)?.cursor}I.info("backfill completed for model {modelID}: {total} documents indexed",{modelID:t,total:String(n)})}v.events.on("document:saved",async a=>{let t=a.document;for(let[a,e]of Object.entries(j)){let a=e[t.model];if(a&&a.length>0)try{null===t.data?await v.removeSearchEntry(t.model,t.id):await v.updateSearchEntry(t.model,t.id,t.data,a)}catch(a){I.error("failed to update search index for document {id}: {err}",{id:t.id,err:String(a)})}}});let M={};async function q(a){return null==M[a]&&(M[a]=v.getGraph(a).then(t=>{if(null==t)throw I.warn("graph {id} not found",{id:a}),delete M[a],Error(`Graph not found: ${a}`);if(I.debug("cached model for graph {id}",{id:a}),t.search){let e={};for(let[a,r]of Object.entries(t.search))e[a]=r.fields??E(t.record[a]);j[a]=e}return{record:t.record,aliases:t.aliases}})),await M[a]}let G={};async function C(a){return null==G[a]&&(G[a]=q(a).then(t=>{let e=n(t);return I.debug("cached schema for graph {id}",{id:a}),e}).catch(t=>{throw delete G[a],t})),await G[a]}async function N(a){let t=await m(g(a));return I.trace("executed GraphQL query {text} with variables {variables}, result: {result}",{text:a.text,variables:a.variables,result:t}),D(t)}return{"graph/deploy":async a=>{let t=u.fromClusters({clusters:a.param.clusters}),e=a.param.search,r=await v.createGraph({id:a.param.id,name:a.param.name,record:t.record,search:e});if(I.info("deployed graph {id}",{id:r}),e){for(let[a,n]of Object.entries(e)){let e=n.fields??E(t.record[a]);e.length>0&&(await v.createSearchIndex(a,e),I.info("created search index for model {modelID}",{modelID:a}),O(r,a,e).catch(t=>{I.error("backfill failed for model {modelID}: {err}",{modelID:a,err:String(t)})}))}let a={};for(let[r,n]of Object.entries(e))a[r]=n.fields??E(t.record[r]);j[r]=a}return delete M[r],delete G[r],{id:r,...t.toJSON(),search:e}},"graph/list":async()=>({graphs:(await v.listGraphs()).map(a=>({id:a.id,name:a.name}))}),"graph/load":async a=>await q(a.param.id),"graph/mutate":async e=>{let n=Object.entries(e.param.attachments??{}).map(([e,r])=>({id:t(o.fromString(e).digest),data:a(r)}));0!==n.length&&await v.addAttachments(n);let d=e.message.payload,u=d.sub||d.iss,m=f(u,d.cap?Array.isArray(d.cap)?d.cap:[d.cap]:void 0,v,k);if(null==e.param.mutations){let a;if(null==T)throw Error("Delegated mutations are not enabled on this server");if(u!==T.id)throw Error("Delegated mutations are only allowed for the server identity");let t=await C(e.param.id);return await v.withTransaction(async n=>{let o={db:n,validators:{}},d=c({issuer:T.id,hlc:A,async processSetMutation(a,t){let e=r(await T.signToken(a));if(null!=t){let r=S.getTransaction(t);if(null==r)throw Error(`Transaction not found or expired: ${t}`);let n=i.fromString(a.sub),o={id:a.sub,model:n.model.toString(),owner:a.aud??a.iss,data:a.data,createdAt:new Date,updatedAt:null};return r.mutations.push({mutation:a,jwt:e,authorDID:a.iss,documentID:o.id,result:o}),o}let s=await l(o,a);return await b({db:n,documentID:s.id,mutationPayload:e,authorDID:a.iss,hlc:a.hlc}),s},async processChangeMutation(a,t){let e=r(await T.signToken(a));if(null!=t){let r,o=S.getTransaction(t);if(null==o)throw Error(`Transaction not found or expired: ${t}`);let s=i.fromString(a.sub),l=await n.getDocument(s);if(1===a.patch.length&&"replace"===a.patch[0].op&&"/"===a.patch[0].path&&null===a.patch[0].value)r={id:a.sub,model:s.model.toString(),owner:l?.owner??a.iss,data:null,createdAt:l?.createdAt??new Date,updatedAt:new Date};else if(null!=l){let t={...l.data??{}};for(let e of a.patch)"value"in e&&"/"!==e.path&&(t[e.path.slice(1)]=e.value);r={...l,data:t,updatedAt:new Date}}else throw Error(`Document not found: ${a.sub}`);return o.mutations.push({mutation:a,jwt:e,authorDID:a.iss,documentID:r.id,result:r}),r}let l=await s(o,a);return await b({db:n,documentID:l.id,mutationPayload:e,authorDID:a.iss,hlc:a.hlc}),l}});if(a=await N({schema:t,type:p.MUTATION,text:e.param.text,variables:e.param.variables??{},context:{db:v,transactionalDB:n,mutationOperations:d,viewerDID:u,accessChecker:m,transactionManager:S}}),a.errors?.length)throw Error("GraphQL mutation errors — rolling back transaction")}),a}let h=e.param.mutations,w={},g={};return await v.withTransaction(async a=>{for(let[t,e]of Object.entries(h))w[t]=await y({db:a,validators:g},e)}),await N({schema:await C(e.param.id),type:p.MUTATION,text:e.param.text,variables:e.param.variables??{},context:{db:v,mutatedDocuments:w,viewerDID:u,accessChecker:m,transactionManager:S}})},"graph/query":async a=>{let t=a.message.payload,e=t.sub||t.iss,r=f(e,t.cap?Array.isArray(t.cap)?t.cap:[t.cap]:void 0,v,k);return await N({schema:await C(a.param.id),type:p.QUERY,text:a.param.text,variables:a.param.variables??{},context:{db:v,viewerDID:e,accessChecker:r}})},"graph/beginTransaction":async a=>({transactionID:S.beginTransaction().id}),"graph/commitTransaction":async a=>{let t=S.getTransaction(a.param.transactionID);if(null==t)throw Error("Transaction not found or expired");return await w({db:v,mutations:t.mutations}),S.markCommitted(t.id),{success:!0}},"graph/rollbackTransaction":async a=>(S.rollbackTransaction(a.param.transactionID),{success:!0}),"graph/subscribe":async a=>{let t=a.message.payload,r=t.sub||t.iss,n=f(r,t.cap?Array.isArray(t.cap)?t.cap:[t.cap]:void 0,v,k),o=g({schema:await C(a.param.id),type:p.SUBSCRIPTION,text:a.param.text,variables:a.param.variables??{},context:{db:v,viewerDID:r,accessChecker:n}}),i=await h(o);if(a.signal.aborted)return null;if("errors"in i)return D(i);let s=a.writable.getWriter();try{await e(i,async a=>{await s.write(D(a))},a.signal)}catch(a){if("Close"!==a)throw a}finally{await s.close()}return null}}}
1
+ import{fromB64 as a,toB64 as e}from"@enkaku/codec";import{consume as r}from"@enkaku/generator";import{getActiveTraceContext as t,withSpan as n}from"@enkaku/otel";import{ValidationError as o}from"@enkaku/schema";import{stringifyToken as i}from"@enkaku/token";import{createSchema as s}from"@kubun/graphql";import{AttachmentID as l,DocumentID as c}from"@kubun/id";import{applyChangeMutation as d,applySetMutation as u,createMutationOperations as m,HLC as p}from"@kubun/mutation";import{GraphModel as h,KubunAttributeKeys as f,KubunSpanNames as g}from"@kubun/protocol";import{execute as w,GraphQLError as y,OperationTypeNode as b,subscribe as D}from"graphql";import{createAccessChecker as I}from"../data/access-control.js";import{applyMutationsAtomically as x}from"../data/apply-atomic.js";import{getExecutionArgs as v}from"../data/graphql.js";import{captureMutation as E}from"../data/mutation-capture.js";import{applyMutation as T}from"../data/mutations.js";function A(a){let e={data:a.data};return null!=a.errors&&(e.errors=a.errors.map(a=>{var e;let r,t,n=a.toJSON();return n.extensions={...n.extensions,...(t={code:r=function(a){let e=a.originalError;if(e instanceof o)return"KB01";let r=e?.message??a.message;return r.includes("not found")?"KB02":r.includes("not allowed")||r.includes("not enabled")?"KB03":r.includes("Invalid GraphQL")||r.includes("expected")?"KB04":r.includes("Transaction")||r.includes("transaction")?"KB05":"KB06"}(e=a)},"KB01"===r&&e.originalError instanceof o&&(t.issues=e.originalError.issues.map(a=>({path:[...a.path],message:a.message,keyword:a.details.keyword,params:a.details.params}))),t)},n})),null!=a.extensions&&(e.extensions=a.extensions),e}export function createHandlers(o){let{db:S,debug:k,logger:O,serverAccessConfig:G,signingIdentity:P,tracer:R,transactionManager:j}=o,B=new p({nodeID:"server"}),Q={};function C(a){return a?.fieldsMeta?Object.entries(a.fieldsMeta).filter(([a,e])=>!0===e.searchable).map(([a])=>a):[]}async function M(a,e,r){let t;O.info("starting backfill for model {modelID} in graph {graphID}",{modelID:e,graphID:a});let n=0,o=!0;for(;o;){let a=await S.queryDocuments({modelIDs:[e],first:100,after:t});for(let t of a.entries)t.document.data&&(await S.updateSearchEntry(e,t.document.id,t.document.data,r),n++);o=a.hasMore,t=a.entries.at(-1)?.cursor}O.info("backfill completed for model {modelID}: {total} documents indexed",{modelID:e,total:String(n)})}S.events.on("document:saved",async a=>{let e=a.document;for(let[a,r]of Object.entries(Q)){let a=r[e.model];if(a&&a.length>0)try{null===e.data?await S.removeSearchEntry(e.model,e.id):await S.updateSearchEntry(e.model,e.id,e.data,a)}catch(a){O.error("failed to update search index for document {id}: {err}",{id:e.id,err:String(a)})}}});let H={};async function _(a){return null==H[a]&&(H[a]=S.getGraph(a).then(e=>{if(null==e)throw O.warn("graph {id} not found",{id:a}),delete H[a],Error(`Graph not found: ${a}`);if(O.debug("cached model for graph {id}",{id:a}),e.search){let r={};for(let[a,t]of Object.entries(e.search))r[a]=t.fields??C(e.record[a]);Q[a]=r}return{record:e.record,aliases:e.aliases}})),await H[a]}let L={};async function U(a){return null==L[a]&&(L[a]=_(a).then(e=>{let r=s(e);return O.debug("cached schema for graph {id}",{id:a}),r}).catch(e=>{throw delete L[a],e})),await L[a]}async function K(a){let e=[],r={documentsScanned:0,modelsQueried:new Set,resolverCount:0,...k?{resolverTimings:new Map}:{}},o=performance.now(),i={[f.GRAPHQL_OPERATION_TYPE]:a.type,[f.GRAPHQL_DOCUMENT]:a.text,[f.GRAPH_ID]:a.graphID};k&&(i[f.GRAPHQL_VARIABLES]=JSON.stringify(a.variables));let s=await n(R,g.GRAPHQL_EXECUTE,{attributes:i},async()=>{let t;try{t=v({...a,context:{...a.context,warnings:e,complexity:r}})}catch(a){return{data:null,errors:[new y(a instanceof Error?a.message:String(a),{originalError:a instanceof Error?a:void 0})]}}return await w(t)}),l=Math.round((performance.now()-o)*100)/100;O.trace("executed GraphQL query {text} with variables {variables}, result: {result}",{text:a.text,variables:a.variables,result:s});let c=A(s),d={documentsScanned:r.documentsScanned,modelsQueried:[...r.modelsQueried],resolverCount:r.resolverCount};null!=r.resolverTimings&&(d.resolverTimings=Object.fromEntries(r.resolverTimings));let u={...c.extensions,timingMS:l,complexity:d},m=t();return null!=m&&(u.traceID=m.traceID,u.spanID=m.spanID),e.length>0&&(u.warnings=e),c.extensions=u,c}return{"graph/deploy":async a=>await n(R,g.GRAPH_DEPLOY,{},async()=>{let e=h.fromClusters({clusters:a.param.clusters}),r=a.param.search,t=await S.createGraph({id:a.param.id,name:a.param.name,record:e.record,search:r});if(O.info("deployed graph {id}",{id:t}),r){for(let[a,n]of Object.entries(r)){let r=n.fields??C(e.record[a]);r.length>0&&(await S.createSearchIndex(a,r),O.info("created search index for model {modelID}",{modelID:a}),M(t,a,r).catch(e=>{O.error("backfill failed for model {modelID}: {err}",{modelID:a,err:String(e)})}))}let a={};for(let[t,n]of Object.entries(r))a[t]=n.fields??C(e.record[t]);Q[t]=a}return delete H[t],delete L[t],{id:t,...e.toJSON(),search:r}}),"graph/list":async()=>({graphs:(await S.listGraphs()).map(a=>({id:a.id,name:a.name}))}),"graph/load":async a=>await _(a.param.id),"graph/mutate":async r=>await n(R,g.GRAPH_MUTATE,{},async()=>{let t=Object.entries(r.param.attachments??{}).map(([r,t])=>({id:e(l.fromString(r).digest),data:a(t)}));0!==t.length&&await S.addAttachments(t);let n=r.message.payload,o=n.sub||n.iss,s=I(o,n.cap?Array.isArray(n.cap)?n.cap:[n.cap]:void 0,S,G);if(null==r.param.mutations){let a;if(null==P)throw Error("Delegated mutations are not enabled on this server");if(o!==P.id)throw Error("Delegated mutations are only allowed for the server identity");let e=await U(r.param.id);try{await S.withTransaction(async t=>{let n={db:t,validators:{}},l=m({issuer:P.id,hlc:B,async processSetMutation(a,e){let r=await P.signToken(a),o=i(r);if(null!=e){let r=j.getTransaction(e);if(null==r)throw Error(`Transaction not found or expired: ${e}`);let t=c.fromString(a.sub),n={id:a.sub,model:t.model.toString(),owner:a.aud??a.iss,data:a.data,createdAt:new Date,updatedAt:null};return r.mutations.push({mutation:a,jwt:o,authorDID:a.iss,documentID:n.id,result:n}),n}let s=await u(n,a);return await E({db:t,documentID:s.id,mutationPayload:o,authorDID:a.iss,hlc:a.hlc}),s},async processChangeMutation(a,e){let r=await P.signToken(a),o=i(r);if(null!=e){let r,n=j.getTransaction(e);if(null==n)throw Error(`Transaction not found or expired: ${e}`);let i=c.fromString(a.sub),s=await t.getDocument(i);if(1===a.patch.length&&"replace"===a.patch[0].op&&"/"===a.patch[0].path&&null===a.patch[0].value)r={id:a.sub,model:i.model.toString(),owner:s?.owner??a.iss,data:null,createdAt:s?.createdAt??new Date,updatedAt:new Date};else if(null!=s){let e={...s.data??{}};for(let r of a.patch)"value"in r&&"/"!==r.path&&(e[r.path.slice(1)]=r.value);r={...s,data:e,updatedAt:new Date}}else throw Error(`Document not found: ${a.sub}`);return n.mutations.push({mutation:a,jwt:o,authorDID:a.iss,documentID:r.id,result:r}),r}let s=await d(n,a);return await E({db:t,documentID:s.id,mutationPayload:o,authorDID:a.iss,hlc:a.hlc}),s}});if(a=await K({schema:e,type:b.MUTATION,text:r.param.text,variables:r.param.variables??{},graphID:r.param.id,context:{db:S,transactionalDB:t,mutationOperations:l,viewerDID:o,accessChecker:s,transactionManager:j,tracer:R}}),a.errors?.length)throw Error("GraphQL mutation errors — rolling back transaction")})}catch(e){if(a?.errors?.some(a=>null!=a.extensions&&"KB01"===a.extensions.code))return a;throw O.warn("delegated mutation failed for graph {graphID}: {error}",{graphID:r.param.id,error:e instanceof Error?e.message:String(e)}),e}return a}let p=r.param.mutations,h={},f={};return await S.withTransaction(async a=>{for(let[e,t]of Object.entries(p))try{h[e]=await T({db:a,validators:f},t)}catch(a){throw O.warn("pre-signed mutation {key} failed for graph {graphID}: {error}",{key:e,graphID:r.param.id,error:a instanceof Error?a.message:String(a)}),a}}),await K({schema:await U(r.param.id),type:b.MUTATION,text:r.param.text,variables:r.param.variables??{},graphID:r.param.id,context:{db:S,mutatedDocuments:h,viewerDID:o,accessChecker:s,transactionManager:j,tracer:R}})}),"graph/query":async a=>await n(R,g.GRAPH_QUERY,{},async()=>{let e=a.message.payload,r=e.sub||e.iss,t=I(r,e.cap?Array.isArray(e.cap)?e.cap:[e.cap]:void 0,S,G);return await K({schema:await U(a.param.id),type:b.QUERY,text:a.param.text,variables:a.param.variables??{},graphID:a.param.id,context:{db:S,viewerDID:r,accessChecker:t,tracer:R}})}),"graph/beginTransaction":async a=>({transactionID:j.beginTransaction().id}),"graph/commitTransaction":async a=>{let e=j.getTransaction(a.param.transactionID);if(null==e)throw Error("Transaction not found or expired");return await x({db:S,mutations:e.mutations}),j.markCommitted(e.id),{success:!0}},"graph/rollbackTransaction":async a=>(j.rollbackTransaction(a.param.transactionID),{success:!0}),"graph/subscribe":async a=>await n(R,g.GRAPH_SUBSCRIBE,{},async()=>{let e=a.message.payload,t=e.sub||e.iss,n=I(t,e.cap?Array.isArray(e.cap)?e.cap:[e.cap]:void 0,S,G),o=v({schema:await U(a.param.id),type:b.SUBSCRIPTION,text:a.param.text,variables:a.param.variables??{},context:{db:S,viewerDID:t,accessChecker:n,tracer:R}}),i=await D(o);if(a.signal.aborted)return null;if("errors"in i)return A(i);let s=a.writable.getWriter();try{await r(i,async a=>{await s.write(A(a))},a.signal)}catch(a){if("Close"!==a)throw a}finally{await s.close()}return null})}}
@@ -1,3 +1,4 @@
1
+ import type { Tracer } from '@enkaku/otel';
1
2
  import type { SigningIdentity } from '@enkaku/token';
2
3
  import type { KubunDB } from '@kubun/db';
3
4
  import type { Logger } from '@kubun/logger';
@@ -5,8 +6,10 @@ import type { ServerAccessConfig } from '../data/access-control.js';
5
6
  import type { TransactionManager } from '../data/transaction-manager.js';
6
7
  export type CreateHandlersParams = {
7
8
  db: KubunDB;
9
+ debug: boolean;
8
10
  logger: Logger;
9
11
  serverAccessConfig: ServerAccessConfig;
10
12
  signingIdentity?: SigningIdentity;
13
+ tracer: Tracer;
11
14
  transactionManager: TransactionManager;
12
15
  };
package/lib/server.d.ts CHANGED
@@ -1,6 +1,7 @@
1
+ import { type Tracer } from '@enkaku/otel';
1
2
  import { type Server } from '@enkaku/server';
2
3
  import { type Identity } from '@enkaku/token';
3
- import { type ClientParams, KubunClient } from '@kubun/client';
4
+ import { type ClientParams, type GetRandomValues, KubunClient } from '@kubun/client';
4
5
  import { type DBParams, KubunDB } from '@kubun/db';
5
6
  import { type Logger } from '@kubun/logger';
6
7
  import type { ClientTransport, Protocol, ServerTransport } from '@kubun/protocol';
@@ -8,10 +9,14 @@ import { SyncManager } from './sync/sync-manager.js';
8
9
  export type ServerParams = {
9
10
  access?: Record<string, boolean | Array<string>>;
10
11
  db: KubunDB | DBParams;
12
+ fetch?: typeof globalThis.fetch;
11
13
  identity: Identity;
12
14
  getRandomID?: () => string;
15
+ getRandomValues?: GetRandomValues;
13
16
  logger?: Logger;
14
17
  allowDelegatedMutations?: boolean;
18
+ tracer?: Tracer;
19
+ debug?: boolean;
15
20
  transactionConfig?: {
16
21
  perConnectionLimit?: number;
17
22
  globalLimit?: number;
package/lib/server.js CHANGED
@@ -1 +1 @@
1
- import{serve as e}from"@enkaku/server";import{isSigningIdentity as t}from"@enkaku/token";import{DirectTransports as r}from"@enkaku/transport";import{KubunClient as s}from"@kubun/client";import{KubunDB as n}from"@kubun/db";import{getKubunLogger as i}from"@kubun/logger";import{TransactionManager as a}from"./data/transaction-manager.js";import{createHandlers as o}from"./handlers/index.js";import{SyncManager as l}from"./sync/sync-manager.js";export class KubunServer{#e;#t;#r;#s;#n;#i;#a;#o;#l;constructor(e){let r,{access:s,db:c,identity:d}=e,g=e.logger??i("server",{serverID:d.id});if(this.#e=s??{},this.#t=c instanceof n?c:new n(c),this.#r=e.getRandomID??(()=>globalThis.crypto.randomUUID()),this.#n=d,this.#i=g,this.#a={read:e.defaultAccessLevel?.read??"only_owner",write:e.defaultAccessLevel?.write??"only_owner"},e.allowDelegatedMutations){if(!t(d))throw Error("allowDelegatedMutations requires a SigningIdentity");r=d}this.#l=new a({getRandomID:this.#r,...e.transactionConfig}),this.#s=o({db:this.#t,logger:g,serverAccessConfig:{defaultAccessLevel:this.#a},signingIdentity:r,transactionManager:this.#l}),this.#o=new l({db:this.#t,identity:this.#n,logger:this.#i})}get db(){return this.#t}get defaultAccessLevel(){return this.#a}get sync(){return this.#o}createClient(e){let{signal:t,...r}=e,[n]=this.serveDirectly(t),i=e.getRandomID??this.#r,a=e.logger??this.#i.getChild("client").with({clientID:i()});return new s({getRandomID:i,logger:a,serverID:this.#n.id,transport:n,...r})}serveDirectly(e){let t=new r({signal:e}),s=this.serve(t.server,e);return[t.client,s]}serve(t,r){return e({access:this.#e,getRandomID:this.#r,handlers:this.#s,identity:this.#n,logger:this.#i,signal:r,transport:t})}}
1
+ import{createTracer as e,traceLogger as t}from"@enkaku/otel";import{serve as r}from"@enkaku/server";import{isSigningIdentity as s}from"@enkaku/token";import{DirectTransports as n}from"@enkaku/transport";import{KubunClient as i}from"@kubun/client";import{KubunDB as a}from"@kubun/db";import{getKubunLogger as o}from"@kubun/logger";import{TransactionManager as l}from"./data/transaction-manager.js";import{createHandlers as d}from"./handlers/index.js";import{SyncManager as c}from"./sync/sync-manager.js";export class KubunServer{#e;#t;#r;#s;#n;#i;#a;#o;#l;constructor(r){let n,{access:i,db:g,identity:h}=r,m=r.logger??o("server",{serverID:h.id}),u=r.tracer??e("kubun.server"),f=r.debug??!1,v=t(m);if(this.#e=i??{},this.#t=g instanceof a?g:new a(g),this.#r=r.getRandomID??(()=>globalThis.crypto.randomUUID()),this.#n=h,this.#i=v,this.#a={read:r.defaultAccessLevel?.read??"only_owner",write:r.defaultAccessLevel?.write??"only_owner"},r.allowDelegatedMutations){if(!s(h))throw Error("allowDelegatedMutations requires a SigningIdentity");n=h}this.#l=new l({getRandomID:this.#r,...r.transactionConfig}),this.#s=d({db:this.#t,debug:f,logger:v,serverAccessConfig:{defaultAccessLevel:this.#a},signingIdentity:n,tracer:u,transactionManager:this.#l}),this.#o=new c({db:this.#t,fetch:r.fetch,getRandomID:r.getRandomID,getRandomValues:r.getRandomValues,identity:this.#n,logger:this.#i})}get db(){return this.#t}get defaultAccessLevel(){return this.#a}get sync(){return this.#o}createClient(e){let{signal:t,...r}=e,[s]=this.serveDirectly(t),n=e.getRandomID??this.#r,a=e.logger??this.#i.getChild("client").with({clientID:n()});return new i({getRandomID:n,logger:a,serverID:this.#n.id,transport:s,...r})}serveDirectly(e){let t=new n({signal:e}),r=this.serve(t.server,e);return[t.client,r]}serve(e,t){return r({accessControl:this.#e,getRandomID:this.#r,handlers:this.#s,identity:this.#n,logger:this.#i,signal:t,transport:e})}}
@@ -1 +1 @@
1
- import{nanoid as e}from"nanoid";export class PeerRegistry{db;constructor(e){this.db=e}async addPeer(t){let r=await this.db.getDB();await r.insertInto("kubun_sync_peers").values({id:e(),peer_did:t.peerDID,endpoint:t.endpoint,mode:t.mode,allowed_users:JSON.stringify(t.allowedUsers),priority:t.priority,trust_level:t.trustLevel,config:JSON.stringify({}),created_at:Date.now(),updated_at:Date.now()}).execute()}async getPeer(e){let t=await this.db.getDB(),r=await t.selectFrom("kubun_sync_peers").selectAll().where("peer_did","=",e).executeTakeFirst();if(r)return{id:r.id,peerDID:r.peer_did,endpoint:r.endpoint,mode:r.mode,allowedUsers:r.allowed_users,priority:r.priority,trustLevel:r.trust_level,createdAt:r.created_at,updatedAt:r.updated_at}}async listPeers(){let e=await this.db.getDB();return(await e.selectFrom("kubun_sync_peers").selectAll().execute()).map(e=>({id:e.id,peerDID:e.peer_did,endpoint:e.endpoint,mode:e.mode,allowedUsers:e.allowed_users,priority:e.priority,trustLevel:e.trust_level,createdAt:e.created_at,updatedAt:e.updated_at}))}async updatePeer(e,t){let r=await this.getPeer(e);if(!r)throw Error(`Peer ${e} not found`);let i={...r,...t},d=await this.db.getDB();await d.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.db.getDB();await t.deleteFrom("kubun_sync_peers").where("peer_did","=",e).execute()}async isPeerAllowed(e){return void 0!==await this.getPeer(e)}}
1
+ import{nanoid as e}from"nanoid";export class PeerRegistry{db;constructor(e){this.db=e}async addPeer(t){let r=await this.db.getDB();await r.insertInto("kubun_sync_peers").values({id:e(),peer_did:t.peerDID,endpoint:t.endpoint,mode:t.mode,allowed_users:JSON.stringify(t.allowedUsers),priority:t.priority,trust_level:t.trustLevel,config:JSON.stringify({}),created_at:Date.now(),updated_at:Date.now()}).onConflict(e=>e.column("peer_did").doUpdateSet({endpoint:t.endpoint,mode:t.mode,allowed_users:JSON.stringify(t.allowedUsers),priority:t.priority,trust_level:t.trustLevel,updated_at:Date.now()})).execute()}async getPeer(e){let t=await this.db.getDB(),r=await t.selectFrom("kubun_sync_peers").selectAll().where("peer_did","=",e).executeTakeFirst();if(r)return{id:r.id,peerDID:r.peer_did,endpoint:r.endpoint,mode:r.mode,allowedUsers:r.allowed_users,priority:r.priority,trustLevel:r.trust_level,createdAt:r.created_at,updatedAt:r.updated_at}}async listPeers(){let e=await this.db.getDB();return(await e.selectFrom("kubun_sync_peers").selectAll().execute()).map(e=>({id:e.id,peerDID:e.peer_did,endpoint:e.endpoint,mode:e.mode,allowedUsers:e.allowed_users,priority:e.priority,trustLevel:e.trust_level,createdAt:e.created_at,updatedAt:e.updated_at}))}async updatePeer(e,t){let r=await this.getPeer(e);if(!r)throw Error(`Peer ${e} not found`);let d={...r,...t},i=await this.db.getDB();await i.updateTable("kubun_sync_peers").set({endpoint:d.endpoint,mode:d.mode,allowed_users:JSON.stringify(d.allowedUsers),priority:d.priority,trust_level:d.trustLevel,config:JSON.stringify({}),updated_at:Date.now()}).where("peer_did","=",e).execute()}async removePeer(e){let t=await this.db.getDB();await t.deleteFrom("kubun_sync_peers").where("peer_did","=",e).execute()}async isPeerAllowed(e){return void 0!==await this.getPeer(e)}}
@@ -1,9 +1,13 @@
1
+ import { type FetchFunction } from '@enkaku/http-client-transport';
1
2
  import type { SigningIdentity } from '@enkaku/token';
2
- import { KubunClient } from '@kubun/client';
3
+ import { type GetRandomValues, KubunClient } from '@kubun/client';
3
4
  import type { Logger } from '@kubun/logger';
4
5
  import type { KubunServer } from '../server.js';
5
6
  export type SyncClientParams = {
7
+ fetch?: FetchFunction;
6
8
  identity: SigningIdentity;
9
+ getRandomID?: () => string;
10
+ getRandomValues?: GetRandomValues;
7
11
  logger: Logger;
8
12
  serverResolver?: (serverID: string) => KubunServer | undefined;
9
13
  };
@@ -1 +1 @@
1
- import{ClientTransport as e}from"@enkaku/http-client-transport";import{createArraySink as t}from"@enkaku/stream";import{DirectTransports as r}from"@enkaku/transport";import{KubunClient as n}from"@kubun/client";export class SyncClient{#e;#t;#r;constructor(e){this.#e=e.identity,this.#t=e.logger,this.#r=e.serverResolver}async connect(e,t){if(e.startsWith("direct://"))return this.#n(e);if(e.startsWith("http://")||e.startsWith("https://"))return this.#i(e,t);throw Error(`Unsupported endpoint scheme: ${e}`)}#i(t,r){let i=new e({url:t});return new n({identity:this.#e,logger:this.#t.getChild("sync-client"),serverID:r,transport:i})}#n(e){let t=e.replace("direct://","");if(!this.#r)throw Error("Server resolver not configured for direct transport");let i=this.#r(t);if(!i)throw Error(`Server not found: ${t}`);let s=new r;return i.serve(s.server),new n({identity:this.#e,logger:this.#t.getChild("sync-client"),serverID:t,transport:s.client})}async negotiate(e,t,r=[]){return e.client.request("sync/negotiate",{param:{scopes:t,delegationTokens:r}})}async merkleSync(e,r){let[n,i]=t(),s=e.client.createStream("sync/merkle-sync",{param:{scopes:r.scopes,excludedDocumentIDs:r.excludedDocumentIDs,tree:r.tree}});s.readable.pipeTo(n),await s;let o=await i,c=[],l=0;for(let e of o)"mutations"===e.type?c.push(...e.mutationJWTs):"complete"===e.type&&(l=e.divergentBuckets??0);return{mutationJWTs:c,divergentBuckets:l}}}
1
+ import{ClientTransport as e}from"@enkaku/http-client-transport";import{createArraySink as t}from"@enkaku/stream";import{DirectTransports as r}from"@enkaku/transport";import{KubunClient as n}from"@kubun/client";export class SyncClient{#e;#t;#r;#n;#s;#o;constructor(e){this.#e=e.fetch,this.#t=e.identity,this.#r=e.getRandomID,this.#n=e.getRandomValues,this.#s=e.logger,this.#o=e.serverResolver}async connect(e,t){if(e.startsWith("direct://"))return this.#i(e);if(e.startsWith("http://")||e.startsWith("https://"))return this.#c(e,t);throw Error(`Unsupported endpoint scheme: ${e}`)}#c(t,r){let s=new e({url:t,fetch:this.#e});return new n({getRandomID:this.#r,getRandomValues:this.#n,identity:this.#t,logger:this.#s.getChild("sync-client"),serverID:r,transport:s})}#i(e){let t=e.replace("direct://","");if(!this.#o)throw Error("Server resolver not configured for direct transport");let s=this.#o(t);if(!s)throw Error(`Server not found: ${t}`);let o=new r;return s.serve(o.server),new n({getRandomID:this.#r,getRandomValues:this.#n,identity:this.#t,logger:this.#s.getChild("sync-client"),serverID:t,transport:o.client})}async negotiate(e,t,r=[]){this.#s.debug("Negotiate: sending {scopeCount} scopes",{scopeCount:t.length});let n=await e.client.request("sync/negotiate",{param:{scopes:t,delegationTokens:r}});return this.#s.debug("Negotiate: accepted {acceptedCount} scopes, excluded {excludedCount} docs",{acceptedCount:n.acceptedScopes.length,excludedCount:n.excludedDocumentIDs.length}),n}async merkleSync(e,r){this.#s.debug("MerkleSync: creating stream with {buckets} local buckets",{buckets:Object.keys(r.tree).length});let[n,s]=t(),o=e.client.createStream("sync/merkle-sync",{param:{scopes:r.scopes,excludedDocumentIDs:r.excludedDocumentIDs,tree:r.tree}}),i=o.readable.pipeTo(n);this.#s.debug("MerkleSync: awaiting stream completion"),await Promise.all([o,i]),this.#s.debug("MerkleSync: stream completed, collecting messages");let c=await s,a=[],l=0;for(let e of c)"mutations"===e.type?a.push(...e.mutationJWTs):"complete"===e.type&&(l=e.divergentBuckets??0);return{mutationJWTs:a,divergentBuckets:l}}}
@@ -1,4 +1,5 @@
1
1
  import { type Identity, type SigningIdentity } from '@enkaku/token';
2
+ import type { GetRandomValues } from '@kubun/client';
2
3
  import type { KubunDB } from '@kubun/db';
3
4
  import type { Logger } from '@kubun/logger';
4
5
  import type { KubunServer } from '../server.js';
@@ -27,6 +28,9 @@ export type SyncEvents = {
27
28
  };
28
29
  export type SyncManagerParams = {
29
30
  db: KubunDB;
31
+ fetch?: typeof globalThis.fetch;
32
+ getRandomID?: () => string;
33
+ getRandomValues?: GetRandomValues;
30
34
  identity: Identity;
31
35
  logger: Logger;
32
36
  };
@@ -1 +1 @@
1
- import{EventEmitter as e}from"@enkaku/event";import{isSigningIdentity as t}from"@enkaku/token";import{applySyncMutations as r}from"./merkle-apply.js";import{buildMerkleTree as s}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;#r;#s=new e;#i=new Map;#n=new Map;#o;#a;constructor(e){this.#e=e.db,this.#o=e.identity,this.#t=e.logger,this.#r=new i(e.db)}setIdentity(e){this.#o=e}setServerResolver(e){this.#a=e}async addPeer(e){await this.#r.addPeer(e),this.#t.info("Peer added",{peerDID:e.peerDID})}async removePeer(e){await this.#r.removePeer(e),this.#t.info("Peer removed",{peerDID:e})}async updatePeerConfig(e,t){await this.#r.updatePeer(e,t),this.#t.info("Peer updated",{peerDID:e})}async listPeers(){return this.#r.listPeers()}async getPeer(e){return this.#r.getPeer(e)}async merkleSyncWithPeer(e,i,o=[]){let a;this.#t.info("Starting Merkle sync with peer",{peerDID:e,scopes:i});let l=await this.#r.getPeer(e);if(!l)throw Error(`Peer ${e} not found`);let c=`merkle-sync-${e}-${Date.now()}`,g={peerID:e,startTime:Date.now(),documentsAttempted:0,documentsCompleted:0};this.#i.set(c,g),this.#l({type:"started",peerID:e,timestamp:Date.now()});try{let m=this.#o;if(!t(m))throw Error("Signing identity required for Merkle sync");let p=new n({identity:m,logger:this.#t,serverResolver:this.#a});a=await p.connect(l.endpoint,e),this.#t.info("Negotiating sync scopes",{scopes:i});let{acceptedScopes:d,excludedDocumentIDs:y}=await p.negotiate(a,i,o);if(0===d.length)return this.#t.info("No scopes accepted by peer"),this.#n.set(e,Date.now()),this.#l({type:"completed",peerID:e,timestamp:Date.now()}),this.#i.delete(c),{sessionID:c,divergentBuckets:0,messagesReceived:0};this.#t.info("Building local Merkle tree",{acceptedScopes:d});let h=await this.#e.getDocumentIDsForScope(d,y),u=await this.#e.getMutationLogForDocuments(h),v=s(u);this.#t.info("Requesting Merkle sync from peer",{localTreeBuckets:Object.keys(v.buckets).length});let f=await p.merkleSync(a,{scopes:d,excludedDocumentIDs:y,tree:v.buckets});return f.mutationJWTs.length>0&&(this.#t.info("Applying sync mutations",{mutations:f.mutationJWTs.length}),g.documentsCompleted=(await r({db:this.#e,mutationJWTs:f.mutationJWTs})).applied),this.#n.set(e,Date.now()),this.#l({type:"completed",peerID:e,timestamp:Date.now()}),this.#t.info("Merkle sync completed",{divergentBuckets:f.divergentBuckets,mutationsReceived:f.mutationJWTs.length}),{sessionID:c,divergentBuckets:f.divergentBuckets,messagesReceived:f.mutationJWTs.length}}catch(t){throw this.#t.error("Merkle sync failed",{peerDID:e,error:t}),this.#l({type:"error",peerID:e,timestamp:Date.now(),error:t instanceof Error?t.message:String(t)}),t}finally{if(null!=a)try{await a.client.dispose()}catch{}this.#i.delete(c)}}getStatus(e){let t=Array.from(this.#i.values()).filter(t=>!e||t.peerID===e).map(e=>({peerID:e.peerID,startTime:e.startTime,documentsAttempted:e.documentsAttempted,documentsCompleted:e.documentsCompleted})),r={};for(let[t,s]of this.#n.entries())e&&t!==e||(r[t]=s);return{activeSessions:t,lastSyncByPeer:r}}onSyncEvent(e){return this.#s.on("sync",e)}#l(e){this.#s.emit("sync",e).catch(e=>{this.#t.error("Error in sync event listener",{error:e})})}}
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;#o=new e;#a=new Map;#l=new Map;#g;#c;constructor(e){this.#e=e.db,this.#t=e.fetch,this.#s=e.getRandomID,this.#r=e.getRandomValues,this.#g=e.identity,this.#i=e.logger,this.#n=new i(e.db)}setIdentity(e){this.#g=e}setServerResolver(e){this.#c=e}async addPeer(e){await this.#n.addPeer(e),this.#i.info("Peer added",{peerDID:e.peerDID})}async removePeer(e){await this.#n.removePeer(e),this.#i.info("Peer removed",{peerDID:e})}async updatePeerConfig(e,t){await this.#n.updatePeer(e,t),this.#i.info("Peer updated",{peerDID:e})}async listPeers(){return this.#n.listPeers()}async getPeer(e){return this.#n.getPeer(e)}async merkleSyncWithPeer(e,i,o=[]){let a;this.#i.info("Starting Merkle sync with peer",{peerDID:e,scopes:i});let l=await this.#n.getPeer(e);if(!l)throw Error(`Peer ${e} not found`);let g=`merkle-sync-${e}-${Date.now()}`,c={peerID:e,startTime:Date.now(),documentsAttempted:0,documentsCompleted:0};this.#a.set(g,c),this.#m({type:"started",peerID:e,timestamp:Date.now()});try{let m=this.#g;if(!t(m))throw Error("Signing identity required for Merkle sync");let d=new n({fetch:this.#t,identity:m,getRandomID:this.#s,getRandomValues:this.#r,logger:this.#i,serverResolver:this.#c});a=await d.connect(l.endpoint,e),this.#i.info("Negotiating sync scopes",{scopes:i});let{acceptedScopes:h,excludedDocumentIDs:p}=await d.negotiate(a,i,o);if(0===h.length)return this.#i.info("No scopes accepted by peer"),this.#l.set(e,Date.now()),this.#m({type:"completed",peerID:e,timestamp:Date.now()}),this.#a.delete(g),{sessionID:g,divergentBuckets:0,messagesReceived:0};this.#i.info("Building local Merkle tree",{acceptedScopes:h});let y=await this.#e.getDocumentIDsForScope(h,p),u=await this.#e.getMutationLogForDocuments(y),v=r(u);this.#i.info("Requesting Merkle sync from peer",{localTreeBuckets:Object.keys(v.buckets).length});let f=await d.merkleSync(a,{scopes:h,excludedDocumentIDs:p,tree:v.buckets});return f.mutationJWTs.length>0&&(this.#i.info("Applying sync mutations",{mutations:f.mutationJWTs.length}),c.documentsCompleted=(await s({db:this.#e,mutationJWTs:f.mutationJWTs})).applied),this.#l.set(e,Date.now()),this.#m({type:"completed",peerID:e,timestamp:Date.now()}),this.#i.info("Merkle sync completed",{divergentBuckets:f.divergentBuckets,mutationsReceived:f.mutationJWTs.length}),{sessionID:g,divergentBuckets:f.divergentBuckets,messagesReceived:f.mutationJWTs.length}}catch(t){throw this.#i.error("Merkle sync failed",{peerDID:e,error:t}),this.#m({type:"error",peerID:e,timestamp:Date.now(),error:t instanceof Error?t.message:String(t)}),t}finally{if(null!=a)try{a.client.abort("SyncComplete"),await a.client.dispose()}catch{}this.#a.delete(g)}}getStatus(e){let t=Array.from(this.#a.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.#o.on("sync",e)}#m(e){this.#o.emit("sync",e).catch(e=>{this.#i.error("Error in sync event listener",{error:e})})}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/server",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
4
4
  "license": "see LICENSE.md",
5
5
  "keywords": [],
6
6
  "type": "module",
@@ -15,38 +15,39 @@
15
15
  ],
16
16
  "sideEffects": false,
17
17
  "dependencies": {
18
- "@enkaku/async": "^0.13.0",
19
- "@enkaku/capability": "^0.13.0",
20
- "@enkaku/codec": "^0.13.0",
21
- "@enkaku/event": "^0.13.0",
22
- "@enkaku/http-client-transport": "^0.13.1",
23
- "@enkaku/generator": "^0.13.0",
24
- "@enkaku/schema": "^0.13.0",
25
- "@enkaku/server": "^0.13.3",
26
- "@enkaku/token": "0.13.0",
27
- "@enkaku/transport": "0.13.1",
18
+ "@enkaku/async": "^0.14.0",
19
+ "@enkaku/capability": "^0.14.0",
20
+ "@enkaku/codec": "^0.14.0",
21
+ "@enkaku/event": "^0.14.1",
22
+ "@enkaku/http-client-transport": "^0.14.2",
23
+ "@enkaku/generator": "^0.14.0",
24
+ "@enkaku/otel": "^0.14.0",
25
+ "@enkaku/schema": "^0.14.0",
26
+ "@enkaku/server": "^0.14.2",
27
+ "@enkaku/token": "0.14.0",
28
+ "@enkaku/transport": "0.14.0",
28
29
  "@noble/hashes": "^2.0.1",
29
30
  "graphql": "^16.13.1",
30
- "@kubun/client": "^0.6.1",
31
- "@kubun/logger": "^0.6.1",
32
- "@kubun/db": "^0.6.0",
33
- "@kubun/id": "^0.6.0",
34
- "@kubun/protocol": "^0.6.0",
35
- "@kubun/graphql": "^0.6.1",
36
- "@kubun/mutation": "^0.6.0"
31
+ "@kubun/client": "^0.7.1",
32
+ "@kubun/graphql": "^0.7.1",
33
+ "@kubun/logger": "^0.7.0",
34
+ "@kubun/id": "^0.7.0",
35
+ "@kubun/mutation": "^0.7.0",
36
+ "@kubun/protocol": "^0.7.1",
37
+ "@kubun/db": "^0.7.0"
37
38
  },
38
39
  "devDependencies": {
39
40
  "@testcontainers/postgresql": "^11.12.0",
40
- "@enkaku/http-server-transport": "^0.13.1",
41
- "@enkaku/stream": "^0.13.0",
42
- "@hono/node-server": "^1.19.10",
41
+ "@enkaku/http-server-transport": "^0.14.1",
42
+ "@enkaku/stream": "^0.14.1",
43
+ "@hono/node-server": "^1.19.11",
43
44
  "get-port": "^7.1.0",
44
45
  "hono": "^4.12.5",
45
46
  "undici": "^7.22.0",
46
- "@kubun/db-better-sqlite": "^0.6.0",
47
- "@kubun/db-postgres": "^0.6.0",
48
- "@kubun/test-utils": "^0.6.0",
49
- "@kubun/scalars": "^0.6.1"
47
+ "@kubun/db-postgres": "^0.7.0",
48
+ "@kubun/scalars": "^0.7.0",
49
+ "@kubun/test-utils": "^0.7.0",
50
+ "@kubun/db-better-sqlite": "^0.7.0"
50
51
  },
51
52
  "scripts": {
52
53
  "build:clean": "del lib",