@kubun/server 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/data/graphql.d.ts +2 -0
- package/lib/data/graphql.js +1 -1
- package/lib/data/mutation-capture.d.ts +3 -1
- package/lib/data/mutation-capture.js +1 -1
- package/lib/data/mutations.js +1 -1
- package/lib/handlers/graph.js +1 -1
- package/lib/handlers/index.js +1 -1
- package/lib/handlers/sync.js +1 -1
- package/lib/handlers/types.d.ts +2 -0
- package/lib/server.d.ts +5 -2
- package/lib/server.js +1 -1
- package/lib/sync/merkle-apply.d.ts +12 -0
- package/lib/sync/merkle-apply.js +1 -0
- package/lib/sync/merkle-tree.d.ts +13 -0
- package/lib/sync/merkle-tree.js +1 -0
- package/lib/sync/sync-client.d.ts +23 -35
- package/lib/sync/sync-client.js +1 -1
- package/lib/sync/sync-manager.d.ts +4 -5
- package/lib/sync/sync-manager.js +1 -1
- package/package.json +14 -13
- package/lib/data/mutation-log.d.ts +0 -17
- package/lib/data/mutation-log.js +0 -1
- package/lib/handlers/document.d.ts +0 -4
- package/lib/handlers/document.js +0 -1
- package/lib/sync/crdt-merge.d.ts +0 -26
- package/lib/sync/crdt-merge.js +0 -1
- package/lib/sync/mutation-replay.d.ts +0 -31
- package/lib/sync/mutation-replay.js +0 -1
package/lib/data/graphql.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { KubunDB } from '@kubun/db';
|
|
2
2
|
import { type Context } from '@kubun/graphql';
|
|
3
|
+
import { type MutationOperations } from '@kubun/mutation';
|
|
3
4
|
import type { DocumentNode } from '@kubun/protocol';
|
|
4
5
|
import { type ExecutionArgs, type GraphQLSchema, type OperationTypeNode } from 'graphql';
|
|
5
6
|
import type { AccessChecker } from './access-control.js';
|
|
@@ -7,6 +8,7 @@ export type ExecutionContext = {
|
|
|
7
8
|
db: KubunDB;
|
|
8
9
|
viewerDID: string;
|
|
9
10
|
mutatedDocuments?: Record<string, DocumentNode>;
|
|
11
|
+
mutationOperations?: MutationOperations<DocumentNode>;
|
|
10
12
|
accessChecker?: AccessChecker;
|
|
11
13
|
};
|
|
12
14
|
export declare function createContext(ctx: ExecutionContext): Context;
|
package/lib/data/graphql.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createReadContext as e}from"@kubun/graphql";import{Kind as
|
|
1
|
+
import{createReadContext as e}from"@kubun/graphql";import{convertPatchInput as t}from"@kubun/mutation";import{Kind as a,parse as n}from"graphql";import{removeDocumentAccessOverride as o,removeModelAccessDefaults as c,setDocumentAccessOverride as i,setModelAccessDefaults as r}from"./mutations.js";export function createContext(a){let n=e({db:a.db,viewerDID:a.viewerDID,accessChecker:a.accessChecker}),u={executeSetModelAccessDefaults:async(e,t,n,o)=>await r({ownerDID:a.viewerDID,modelID:e,permissionType:t,accessLevel:n,allowedDIDs:o},a.db),async executeRemoveModelAccessDefaults(e,t){await c({ownerDID:a.viewerDID,modelID:e,permissionTypes:t},a.db)},executeSetDocumentAccessOverride:async(e,t,n,o)=>await i({documentID:e,permissionType:t,accessLevel:n,allowedDIDs:o},a.db),async executeRemoveDocumentAccessOverride(e,t){await o({documentID:e,permissionTypes:t},a.db)}};if(null!=a.mutationOperations){let e=a.mutationOperations;return{...n,executeCreateMutation:async(t,a)=>await e.createDocument(t,a),executeSetMutation:async(t,a,n)=>await e.setDocument(t,a,n),executeUpdateMutation:async a=>await e.updateDocument(a.id,t(a.patch)),async executeRemoveMutation(t){await e.removeDocument(t)},...u}}function s(e){return a.mutatedDocuments?.[e.path.key]}return{...n,executeCreateMutation:async(e,t,a)=>s(a),executeSetMutation:async(e,t,a,n)=>s(n),executeUpdateMutation:async(e,t)=>s(t),async executeRemoveMutation(e,t){},...u}}export function getExecutionArgs(e){let t=n(e.text),o=t.definitions[0];if(null==o)throw Error("Missing GraphQL document definition");if(o.kind!==a.OPERATION_DEFINITION||o.operation!==e.type)throw Error(`Invalid GraphQL document definition: expected ${e.type} operation`);return{document:t,schema:e.schema,variableValues:e.variables,contextValue:createContext(e.context)}}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { KubunDB } from '@kubun/db';
|
|
2
|
+
export declare function computeMutationHash(jwt: string): string;
|
|
2
3
|
export declare function captureMutation(params: {
|
|
3
4
|
db: KubunDB;
|
|
4
5
|
documentID: string;
|
|
5
6
|
mutationPayload: string;
|
|
6
7
|
authorDID: string;
|
|
7
|
-
|
|
8
|
+
hlc: string;
|
|
9
|
+
}): Promise<string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{DocumentID as t}from"@kubun/id";import{blake3 as o}from"@noble/hashes/blake3.js";let n=new TextEncoder;export function computeMutationHash(t){return Array.from(o(n.encode(t))).map(t=>t.toString(16).padStart(2,"0")).join("")}export async function captureMutation(o){let{db:n,documentID:r,mutationPayload:a,authorDID:e,hlc:i}=o,u=computeMutationHash(a),m=t.fromString(r).model.toString();return await n.insertMutationLogEntry({mutation_hash:u,model_id:m,document_id:r,author_did:e,hlc:i,mutation_jwt:a,status:"applied"}),u}
|
package/lib/data/mutations.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{asType as e,createValidator as t}from"@enkaku/schema";import{verifyToken as o}from"@enkaku/token";import{DocumentID as a}from"@kubun/id";import{applyMutation as s}from"@kubun/mutation";import{documentMutation as
|
|
1
|
+
import{asType as e,createValidator as t}from"@enkaku/schema";import{verifyToken as o}from"@enkaku/token";import{DocumentID as a}from"@kubun/id";import{applyMutation as s,HLC as r}from"@kubun/mutation";import{documentMutation as l}from"@kubun/protocol";import{validateDIDs as n}from"./access-control.js";import{captureMutation as i}from"./mutation-capture.js";let c=t(l),d=new r({nodeID:"server"});export async function applyMutation(t,a){null==t.hlc&&(t.hlc=d),null==t.maxDriftMS&&(t.maxDriftMS=6e4);let r=e(c,(await o(a)).payload),l=await s(t,r);return await i({db:t.db,documentID:l.id,mutationPayload:a,authorDID:r.iss,hlc:r.hlc}),l}export async function setModelAccessDefaults(e,t){let{ownerDID:o,modelID:a,permissionType:s,accessLevel:r,allowedDIDs:l}=e;if(l&&l.length>0&&n(l),!({read:["only_owner","anyone","allowed_dids"],write:["only_owner","allowed_dids"]})[s].includes(r))throw Error(`Invalid access level "${r}" for permission type "${s}"`);await t.setUserModelAccessDefault({ownerDID:o,modelID:a,permissionType:s,accessLevel:r,allowedDIDs:l});let i=await t.getUserModelAccessDefault(o,a,"read"),c=await t.getUserModelAccessDefault(o,a,"write"),d={};return i&&(d.read={level:i.level,allowedDIDs:i.allowedDIDs??[]}),c&&(d.write={level:c.level,allowedDIDs:c.allowedDIDs??[]}),{ownerDID:o,modelId:a,permissions:d}}export async function removeModelAccessDefaults(e,t){let{ownerDID:o,modelID:a,permissionTypes:s}=e;await t.removeUserModelAccessDefaults(o,a,s)}export async function setDocumentAccessOverride(e,t){let{documentID:o,permissionType:s,accessLevel:r,allowedDIDs:l}=e;l&&l.length>0&&n(l);let i=a.fromString(o),c=await t.getDocument(i);if(!c)throw Error(`Document not found: ${o}`);let d={...c.data?.accessPermissions||{},[s]:{level:r,allowedDIDs:l}};return await t.saveDocument({id:i,data:{...c.data,accessPermissions:d},existing:c})}export async function removeDocumentAccessOverride(e,t){let{documentID:o,permissionTypes:s}=e,r=a.fromString(o),l=await t.getDocument(r);if(!l)return;let n={...l.data?.accessPermissions||{}};for(let e of s)delete n[e];await t.saveDocument({id:r,data:{...l.data,accessPermissions:Object.keys(n).length>0?n:void 0},existing:l})}
|
package/lib/handlers/graph.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fromB64 as a,toB64 as e}from"@enkaku/codec";import{consume as
|
|
1
|
+
import{fromB64 as a,toB64 as e}from"@enkaku/codec";import{consume as t}from"@enkaku/generator";import{stringifyToken as r}from"@enkaku/token";import{createSchema as i}from"@kubun/graphql";import{AttachmentID as o}from"@kubun/id";import{applyChangeMutation as n,applySetMutation as s,createMutationOperations as l,HLC as c}from"@kubun/mutation";import{GraphModel as d}from"@kubun/protocol";import{execute as m,OperationTypeNode as u,subscribe as p}from"graphql";import{createAccessChecker as f}from"../data/access-control.js";import{getExecutionArgs as h}from"../data/graphql.js";import{captureMutation as y}from"../data/mutation-capture.js";import{applyMutation as w}from"../data/mutations.js";function g(a){let e={data:a.data};return null!=a.errors&&(e.errors=a.errors.map(a=>a.toJSON())),null!=a.extensions&&(e.extensions=a.extensions),e}export function createHandlers(b){let{db:x,logger:v,serverAccessConfig:k,signingIdentity:D}=b,I=new c({nodeID:"server"}),S={};function O(a){return a?.fieldsMeta?Object.entries(a.fieldsMeta).filter(([a,e])=>!0===e.searchable).map(([a])=>a):[]}async function j(a,e,t){let r;v.info("starting backfill for model {modelID} in graph {graphID}",{modelID:e,graphID:a});let i=0,o=!0;for(;o;){let a=await x.queryDocuments({modelIDs:[e],first:100,after:r});for(let r of a.entries)r.document.data&&(await x.updateSearchEntry(e,r.document.id,r.document.data,t),i++);o=a.hasMore,r=a.entries.at(-1)?.cursor}v.info("backfill completed for model {modelID}: {total} documents indexed",{modelID:e,total:String(i)})}x.events.on("document:saved",async a=>{let e=a.document;for(let[a,t]of Object.entries(S)){let a=t[e.model];if(a&&a.length>0)try{null===e.data?await x.removeSearchEntry(e.model,e.id):await x.updateSearchEntry(e.model,e.id,e.data,a)}catch(a){v.error("failed to update search index for document {id}: {err}",{id:e.id,err:String(a)})}}});let A={};async function E(a){return null==A[a]&&(A[a]=x.getGraph(a).then(e=>{if(null==e)throw v.warn("graph {id} not found",{id:a}),delete A[a],Error(`Graph not found: ${a}`);if(v.debug("cached model for graph {id}",{id:a}),e.search){let t={};for(let[a,r]of Object.entries(e.search))t[a]=r.fields??O(e.record[a]);S[a]=t}return{record:e.record,aliases:e.aliases}})),await A[a]}let M={};async function T(a){return null==M[a]&&(M[a]=E(a).then(e=>{let t=i(e);return v.debug("cached schema for graph {id}",{id:a}),t}).catch(e=>{throw delete M[a],e})),await M[a]}async function q(a){let e=await m(h(a));return v.trace("executed GraphQL query {text} with variables {variables}, result: {result}",{text:a.text,variables:a.variables,result:e}),g(e)}return{"graph/deploy":async a=>{let e=d.fromClusters({clusters:a.param.clusters}),t=a.param.search,r=await x.createGraph({id:a.param.id,name:a.param.name,record:e.record,search:t});if(v.info("deployed graph {id}",{id:r}),t){for(let[a,i]of Object.entries(t)){let t=i.fields??O(e.record[a]);t.length>0&&(await x.createSearchIndex(a,t),v.info("created search index for model {modelID}",{modelID:a}),j(r,a,t).catch(e=>{v.error("backfill failed for model {modelID}: {err}",{modelID:a,err:String(e)})}))}let a={};for(let[r,i]of Object.entries(t))a[r]=i.fields??O(e.record[r]);S[r]=a}return delete A[r],delete M[r],{id:r,...e.toJSON(),search:t}},"graph/list":async()=>({graphs:(await x.listGraphs()).map(a=>({id:a.id,name:a.name}))}),"graph/load":async a=>await E(a.param.id),"graph/mutate":async t=>{let i=Object.entries(t.param.attachments??{}).map(([t,r])=>({id:e(o.fromString(t).digest),data:a(r)}));0!==i.length&&await x.addAttachments(i);let c=t.message.payload,d=c.sub||c.iss,m=f(d,c.cap?Array.isArray(c.cap)?c.cap:[c.cap]:void 0,x,k);if(null==t.param.mutations){if(null==D)throw Error("Delegated mutations are not enabled on this server");if(d!==D.id)throw Error("Delegated mutations are only allowed for the server identity");let a={db:x,validators:{}},e=l({issuer:D.id,hlc:I,async processSetMutation(e){let t=r(await D.signToken(e)),i=await s(a,e);return await y({db:x,documentID:i.id,mutationPayload:t,authorDID:e.iss,hlc:e.hlc}),i},async processChangeMutation(e){let t=r(await D.signToken(e)),i=await n(a,e);return await y({db:x,documentID:i.id,mutationPayload:t,authorDID:e.iss,hlc:e.hlc}),i}});return await q({schema:await T(t.param.id),type:u.MUTATION,text:t.param.text,variables:t.param.variables??{},context:{db:x,mutationOperations:e,viewerDID:d,accessChecker:m}})}let p={},h={};return await Promise.all(Object.entries(t.param.mutations).map(async([a,e])=>{p[a]=await w({db:x,validators:h},e)})),await q({schema:await T(t.param.id),type:u.MUTATION,text:t.param.text,variables:t.param.variables??{},context:{db:x,mutatedDocuments:p,viewerDID:d,accessChecker:m}})},"graph/query":async a=>{let e=a.message.payload,t=e.sub||e.iss,r=f(t,e.cap?Array.isArray(e.cap)?e.cap:[e.cap]:void 0,x,k);return await q({schema:await T(a.param.id),type:u.QUERY,text:a.param.text,variables:a.param.variables??{},context:{db:x,viewerDID:t,accessChecker:r}})},"graph/subscribe":async a=>{let e=a.message.payload,r=e.sub||e.iss,i=f(r,e.cap?Array.isArray(e.cap)?e.cap:[e.cap]:void 0,x,k),o=h({schema:await T(a.param.id),type:u.SUBSCRIPTION,text:a.param.text,variables:a.param.variables??{},context:{db:x,viewerDID:r,accessChecker:i}}),n=await p(o);if(a.signal.aborted)return null;if("errors"in n)return g(n);let s=a.writable.getWriter();try{await t(n,async a=>{await s.write(g(a))},a.signal)}catch(a){if("Close"!==a)throw a}finally{await s.close()}return null}}}
|
package/lib/handlers/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHandlers as r}from"./
|
|
1
|
+
import{createHandlers as r}from"./graph.js";import{createHandlers as e}from"./sync.js";export function createHandlers(a){return{...r(a),...e(a)}}
|
package/lib/handlers/sync.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{checkCapability as e}from"@enkaku/capability";import{
|
|
1
|
+
import{checkCapability as e}from"@enkaku/capability";import{buildMerkleTree as t,findDivergentBuckets as r,getTimeBuckets as n}from"../sync/merkle-tree.js";async function s(t,r,n){if(!n||0===n.length)return!1;let s=["*","urn:kubun:user:*",`urn:kubun:user:${r}`],a="document/read";for(let c of s)try{return await e({act:a,res:c},{iss:t,sub:r,cap:n}),!0}catch{}for(let c of n)for(let n of s)try{return await e({act:a,res:n},{iss:t,sub:r,cap:c}),!0}catch{}return!1}export function createHandlers(e){let{db:a,logger:c}=e;return{"sync/negotiate":async e=>{let{scopes:t,delegationTokens:r}=e.param,n=e.message.payload,a=n.sub||n.iss;c.debug("sync/negotiate requested",{scopes:t,viewerDID:a});let o=[];for(let e of t){let t=a===e.ownerDID,n=!t&&await s(a,e.ownerDID,r);t||n?o.push(e):c.debug("sync/negotiate: scope rejected",{scope:e,viewerDID:a})}return c.info("sync/negotiate completed",{requested:t.length,accepted:o.length}),{acceptedScopes:o,excludedDocumentIDs:[]}},"sync/merkle-sync":async e=>{let{scopes:s,excludedDocumentIDs:o,tree:i}=e.param,u=e.writable.getWriter();c.info("sync/merkle-sync started",{scopes:s,excludedDocumentIDs:o});let l=0,y=0;try{let e=await a.getDocumentIDsForScope(s,o);if(0===e.length)return await u.write({type:"complete",divergentBuckets:0,totalMutations:0}),{success:!0,divergentBuckets:0,mutationsSent:0};let m=await a.getMutationLogForDocuments(e),g=t(m),d={root:i.root??"",buckets:i},p=r(g,d);if(l=p.length,0===p.length)return c.info("sync/merkle-sync: no divergent buckets, already in sync"),await u.write({type:"complete",divergentBuckets:0,totalMutations:0}),{success:!0,divergentBuckets:0,mutationsSent:0};let f=new Set(p),w=m.filter(e=>{let{minute:t}=n(e.hlc);return f.has(t)});for(let e=0;e<w.length;e+=1e3){let t=w.slice(e,e+1e3);await u.write({type:"mutations",mutationJWTs:t.map(e=>e.mutation_jwt)}),y+=t.length}await u.write({type:"complete",divergentBuckets:l,totalMutations:y})}catch(e){throw c.error("sync/merkle-sync error",{error:e}),e}finally{try{await u.close()}catch{}}return c.info("sync/merkle-sync completed",{divergentBuckets:l,mutationsSent:y}),{success:!0,divergentBuckets:l,mutationsSent:y}}}}
|
package/lib/handlers/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SigningIdentity } from '@enkaku/token';
|
|
1
2
|
import type { KubunDB } from '@kubun/db';
|
|
2
3
|
import type { Logger } from '@kubun/logger';
|
|
3
4
|
import type { ServerAccessConfig } from '../data/access-control.js';
|
|
@@ -5,4 +6,5 @@ export type CreateHandlersParams = {
|
|
|
5
6
|
db: KubunDB;
|
|
6
7
|
logger: Logger;
|
|
7
8
|
serverAccessConfig: ServerAccessConfig;
|
|
9
|
+
signingIdentity?: SigningIdentity;
|
|
8
10
|
};
|
package/lib/server.d.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { type Server } from '@enkaku/server';
|
|
2
|
-
import type
|
|
2
|
+
import { type Identity } from '@enkaku/token';
|
|
3
3
|
import { type ClientParams, KubunClient } from '@kubun/client';
|
|
4
4
|
import { type DBParams, KubunDB } from '@kubun/db';
|
|
5
5
|
import { type Logger } from '@kubun/logger';
|
|
6
|
-
import type { Protocol, ServerTransport } from '@kubun/protocol';
|
|
6
|
+
import type { ClientTransport, Protocol, ServerTransport } from '@kubun/protocol';
|
|
7
7
|
import { SyncManager } from './sync/sync-manager.js';
|
|
8
8
|
export type ServerParams = {
|
|
9
9
|
access?: Record<string, boolean | Array<string>>;
|
|
10
10
|
db: KubunDB | DBParams;
|
|
11
11
|
identity: Identity;
|
|
12
|
+
getRandomID?: () => string;
|
|
12
13
|
logger?: Logger;
|
|
14
|
+
allowDelegatedMutations?: boolean;
|
|
13
15
|
defaultAccessLevel?: {
|
|
14
16
|
read?: 'only_owner' | 'anyone' | 'allowed_dids';
|
|
15
17
|
write?: 'only_owner' | 'allowed_dids';
|
|
@@ -28,5 +30,6 @@ export declare class KubunServer {
|
|
|
28
30
|
};
|
|
29
31
|
get sync(): SyncManager;
|
|
30
32
|
createClient(params: CreateClientParams): KubunClient;
|
|
33
|
+
serveDirectly(signal?: AbortSignal): [ClientTransport, Server<Protocol>];
|
|
31
34
|
serve(transport: ServerTransport, signal?: AbortSignal): Server<Protocol>;
|
|
32
35
|
}
|
package/lib/server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{serve as e}from"@enkaku/server";import{
|
|
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 i}from"@kubun/db";import{getKubunLogger as n}from"@kubun/logger";import{createHandlers as o}from"./handlers/index.js";import{SyncManager as l}from"./sync/sync-manager.js";export class KubunServer{#e;#t;#r;#s;#i;#n;#o;#l;constructor(e){let r,{access:s,db:d,identity:a}=e,c=e.logger??n("server",{serverID:a.id});if(this.#e=s??{},this.#t=d instanceof i?d:new i(d),this.#r=e.getRandomID??(()=>globalThis.crypto.randomUUID()),this.#i=a,this.#n=c,this.#o={read:e.defaultAccessLevel?.read??"only_owner",write:e.defaultAccessLevel?.write??"only_owner"},e.allowDelegatedMutations){if(!t(a))throw Error("allowDelegatedMutations requires a SigningIdentity");r=a}this.#s=o({db:this.#t,logger:c,serverAccessConfig:{defaultAccessLevel:this.#o},signingIdentity:r}),this.#l=new l({db:this.#t,identity:this.#i,logger:this.#n})}get db(){return this.#t}get defaultAccessLevel(){return this.#o}get sync(){return this.#l}createClient(e){let{signal:t,...r}=e,[i]=this.serveDirectly(t),n=e.getRandomID??this.#r,o=e.logger??this.#n.getChild("client").with({clientID:n()});return new s({getRandomID:n,logger:o,serverID:this.#i.id,transport:i,...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.#i,logger:this.#n,signal:r,transport:t})}}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { KubunDB } from '@kubun/db';
|
|
2
|
+
export type ApplySyncMutationsParams = {
|
|
3
|
+
db: KubunDB;
|
|
4
|
+
mutationJWTs: Array<string>;
|
|
5
|
+
};
|
|
6
|
+
export type ApplySyncMutationsResult = {
|
|
7
|
+
applied: number;
|
|
8
|
+
rejected: number;
|
|
9
|
+
pending: number;
|
|
10
|
+
skipped: number;
|
|
11
|
+
};
|
|
12
|
+
export declare function applySyncMutations(params: ApplySyncMutationsParams): Promise<ApplySyncMutationsResult>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{asType as t,createValidator as a}from"@enkaku/schema";import{verifyToken as i}from"@enkaku/token";import{DocumentID as o}from"@kubun/id";import{applyMutation as n}from"@kubun/mutation";import{documentMutation as u}from"@kubun/protocol";import{computeMutationHash as e}from"../data/mutation-capture.js";let r=a(u);export async function applySyncMutations(a){let{db:u,mutationJWTs:m}=a,s=0,d=0,c=0,l=0,h={db:u,validators:{}};for(let a of m){let m,p=e(a);if(await u.hasMutationHash(p)){l++;continue}try{let o=await i(a);m=t(r,o.payload)}catch{d++;continue}let _=m.sub,f=o.fromString(_).model.toString(),w=o.fromString(_);if(null==await u.getDocument(w)&&"change"===m.typ){await u.insertMutationLogEntry({mutation_hash:p,model_id:f,document_id:_,author_did:m.iss,hlc:m.hlc,mutation_jwt:a,status:"pending"}),c++;continue}try{if(await n(h,m),await u.insertMutationLogEntry({mutation_hash:p,model_id:f,document_id:_,author_did:m.iss,hlc:m.hlc,mutation_jwt:a,status:"applied"}),s++,"set"===m.typ)for(let a of(await u.getPendingMutations(_)))try{let o=await i(a.mutation_jwt),e=t(r,o.payload);await n(h,e),await u.updateMutationStatus(a.mutation_hash,"applied"),s++}catch{await u.updateMutationStatus(a.mutation_hash,"rejected"),d++}}catch{await u.insertMutationLogEntry({mutation_hash:p,model_id:f,document_id:_,author_did:m.iss,hlc:m.hlc,mutation_jwt:a,status:"rejected"}),d++}}return{applied:s,rejected:d,pending:c,skipped:l}}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MutationLogEntry } from '@kubun/db';
|
|
2
|
+
export type MerkleTree = {
|
|
3
|
+
root: string;
|
|
4
|
+
buckets: Record<string, string>;
|
|
5
|
+
};
|
|
6
|
+
export declare function getTimeBuckets(hlc: string): {
|
|
7
|
+
year: string;
|
|
8
|
+
month: string;
|
|
9
|
+
day: string;
|
|
10
|
+
minute: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function buildMerkleTree(entries: Array<MutationLogEntry>): MerkleTree;
|
|
13
|
+
export declare function findDivergentBuckets(local: MerkleTree, remote: MerkleTree): Array<string>;
|
|
@@ -0,0 +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()}
|
|
@@ -7,33 +7,17 @@ export type SyncClientParams = {
|
|
|
7
7
|
logger: Logger;
|
|
8
8
|
serverResolver?: (serverID: string) => KubunServer | undefined;
|
|
9
9
|
};
|
|
10
|
-
export type
|
|
11
|
-
type?: string;
|
|
12
|
-
documentID?: string;
|
|
13
|
-
syncMode?: string;
|
|
14
|
-
mutationJWTs?: Array<string>;
|
|
15
|
-
fullState?: string;
|
|
16
|
-
reason?: string;
|
|
17
|
-
};
|
|
18
|
-
export type DiscoveryDocument = {
|
|
19
|
-
documentID: string;
|
|
10
|
+
export type SyncScope = {
|
|
20
11
|
modelID: string;
|
|
21
|
-
|
|
22
|
-
checkpointHash: string;
|
|
23
|
-
sequenceNumber: string;
|
|
24
|
-
priority: number;
|
|
25
|
-
};
|
|
26
|
-
export type DiscoveryResult = {
|
|
27
|
-
documents: Array<DiscoveryDocument>;
|
|
12
|
+
ownerDID: string;
|
|
28
13
|
};
|
|
29
|
-
export type
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
vectorClock: Record<string, number>;
|
|
14
|
+
export type MerkleSyncMessage = {
|
|
15
|
+
type: 'mutations';
|
|
16
|
+
mutationJWTs: Array<string>;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'complete';
|
|
19
|
+
divergentBuckets?: number;
|
|
20
|
+
totalMutations?: number;
|
|
37
21
|
};
|
|
38
22
|
/**
|
|
39
23
|
* Client for connecting to peer servers for sync operations.
|
|
@@ -49,18 +33,22 @@ export declare class SyncClient {
|
|
|
49
33
|
*/
|
|
50
34
|
connect(endpoint: string): Promise<KubunClient>;
|
|
51
35
|
/**
|
|
52
|
-
*
|
|
53
|
-
* Documents are returned sorted by priority (most recently modified first).
|
|
36
|
+
* Negotiate sync scopes with peer.
|
|
54
37
|
*/
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
streamDocuments(client: KubunClient, documentIDs: Array<string>, delegationTokens?: Array<string>): Promise<Array<SyncStreamMessage>>;
|
|
38
|
+
negotiate(client: KubunClient, scopes: Array<SyncScope>, delegationTokens?: Array<string>): Promise<{
|
|
39
|
+
acceptedScopes: Array<SyncScope>;
|
|
40
|
+
excludedDocumentIDs: Array<string>;
|
|
41
|
+
}>;
|
|
60
42
|
/**
|
|
61
|
-
*
|
|
43
|
+
* Perform Merkle tree-based sync with peer.
|
|
44
|
+
* Sends local tree, receives divergent mutation JWTs.
|
|
62
45
|
*/
|
|
63
|
-
|
|
64
|
-
|
|
46
|
+
merkleSync(client: KubunClient, params: {
|
|
47
|
+
scopes: Array<SyncScope>;
|
|
48
|
+
excludedDocumentIDs: Array<string>;
|
|
49
|
+
tree: Record<string, string>;
|
|
50
|
+
}): Promise<{
|
|
51
|
+
mutationJWTs: Array<string>;
|
|
52
|
+
divergentBuckets: number;
|
|
65
53
|
}>;
|
|
66
54
|
}
|
package/lib/sync/sync-client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createArraySink as e}from"@enkaku/stream";import{DirectTransports as
|
|
1
|
+
import{createArraySink as e}from"@enkaku/stream";import{DirectTransports as t}from"@enkaku/transport";import{KubunClient as r}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){if(e.startsWith("direct://"))return this.#n(e);throw Error(`HTTP transport not yet implemented: ${e}`)}#n(e){let n=e.replace("direct://","");if(!this.#r)throw Error("Server resolver not configured for direct transport");let o=this.#r(n);if(!o)throw Error(`Server not found: ${n}`);let i=new t;return o.serve(i.server),new r({identity:this.#e,logger:this.#t.getChild("sync-client"),serverID:n,transport:i.client})}async negotiate(e,t,r=[]){return e.client.request("sync/negotiate",{param:{scopes:t,delegationTokens:r}})}async merkleSync(t,r){let[n,o]=e(),i=t.client.createStream("sync/merkle-sync",{param:{scopes:r.scopes,excludedDocumentIDs:r.excludedDocumentIDs,tree:r.tree}});i.readable.pipeTo(n),await i;let s=await o,c=[],l=0;for(let e of s)"mutations"===e.type&&"mutationJWTs"in e?c.push(...e.mutationJWTs):"complete"===e.type&&(l=("divergentBuckets"in e?e.divergentBuckets:0)??0);return{mutationJWTs:c,divergentBuckets:l}}}
|
|
@@ -3,10 +3,7 @@ import type { KubunDB } from '@kubun/db';
|
|
|
3
3
|
import type { Logger } from '@kubun/logger';
|
|
4
4
|
import type { KubunServer } from '../server.js';
|
|
5
5
|
import { type PeerConfig, type PeerConfigWithID } from './peer-registry.js';
|
|
6
|
-
|
|
7
|
-
userIDs?: Array<string>;
|
|
8
|
-
documentIDs?: Array<string>;
|
|
9
|
-
};
|
|
6
|
+
import { type SyncScope } from './sync-client.js';
|
|
10
7
|
export type SyncSessionInfo = {
|
|
11
8
|
peerID: string;
|
|
12
9
|
startTime: number;
|
|
@@ -49,8 +46,10 @@ export declare class SyncManager {
|
|
|
49
46
|
updatePeerConfig(peerDID: string, updates: Partial<PeerConfig>): Promise<void>;
|
|
50
47
|
listPeers(): Promise<Array<PeerConfigWithID>>;
|
|
51
48
|
getPeer(peerDID: string): Promise<PeerConfigWithID | undefined>;
|
|
52
|
-
|
|
49
|
+
merkleSyncWithPeer(peerDID: string, scopes: Array<SyncScope>, delegationTokens?: Array<string>): Promise<{
|
|
53
50
|
sessionID: string;
|
|
51
|
+
divergentBuckets: number;
|
|
52
|
+
messagesReceived: number;
|
|
54
53
|
}>;
|
|
55
54
|
getStatus(peerDID?: string): SyncStatus;
|
|
56
55
|
onSyncEvent(callback: (event: SyncEvent) => void): () => void;
|
package/lib/sync/sync-manager.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{EventEmitter as e}from"@enkaku/event";import{isSigningIdentity as t}from"@enkaku/token";import{
|
|
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=[]){this.#t.info("Starting Merkle sync with peer",{peerDID:e,scopes:i});let a=await this.#r.getPeer(e);if(!a)throw Error(`Peer ${e} not found`);let l=`merkle-sync-${e}-${Date.now()}`,g={peerID:e,startTime:Date.now(),documentsAttempted:0,documentsCompleted:0};this.#i.set(l,g),this.#l({type:"started",peerID:e,timestamp:Date.now()});try{let c=this.#o;if(!t(c))throw Error("Signing identity required for Merkle sync");let m=new n({identity:c,logger:this.#t,serverResolver:this.#a}),p=await m.connect(a.endpoint);this.#t.info("Negotiating sync scopes",{scopes:i});let{acceptedScopes:d,excludedDocumentIDs:y}=await m.negotiate(p,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(l),{sessionID:l,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 m.merkleSync(p,{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:l,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{this.#i.delete(l)}}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})})}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"license": "see LICENSE.md",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"type": "module",
|
|
@@ -21,25 +21,26 @@
|
|
|
21
21
|
"@enkaku/event": "^0.13.0",
|
|
22
22
|
"@enkaku/generator": "^0.13.0",
|
|
23
23
|
"@enkaku/schema": "^0.13.0",
|
|
24
|
-
"@enkaku/server": "^0.13.
|
|
24
|
+
"@enkaku/server": "^0.13.1",
|
|
25
25
|
"@enkaku/token": "0.13.0",
|
|
26
26
|
"@enkaku/transport": "0.13.1",
|
|
27
|
+
"@noble/hashes": "^2.0.1",
|
|
27
28
|
"graphql": "^16.12.0",
|
|
28
|
-
"@kubun/
|
|
29
|
-
"@kubun/
|
|
30
|
-
"@kubun/
|
|
31
|
-
"@kubun/
|
|
32
|
-
"@kubun/
|
|
33
|
-
"@kubun/
|
|
34
|
-
"@kubun/id": "^0.
|
|
29
|
+
"@kubun/client": "^0.6.0",
|
|
30
|
+
"@kubun/db": "^0.6.0",
|
|
31
|
+
"@kubun/protocol": "^0.6.0",
|
|
32
|
+
"@kubun/mutation": "^0.6.0",
|
|
33
|
+
"@kubun/logger": "^0.6.0",
|
|
34
|
+
"@kubun/graphql": "^0.6.0",
|
|
35
|
+
"@kubun/id": "^0.6.0"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@databases/pg-test": "^3.1.2",
|
|
38
39
|
"@enkaku/stream": "^0.13.0",
|
|
39
|
-
"@kubun/db-
|
|
40
|
-
"@kubun/
|
|
41
|
-
"@kubun/test-utils": "^0.
|
|
42
|
-
"@kubun/
|
|
40
|
+
"@kubun/db-postgres": "^0.6.0",
|
|
41
|
+
"@kubun/scalars": "^0.6.0",
|
|
42
|
+
"@kubun/test-utils": "^0.6.0",
|
|
43
|
+
"@kubun/db-better-sqlite": "^0.6.0"
|
|
43
44
|
},
|
|
44
45
|
"scripts": {
|
|
45
46
|
"build:clean": "del lib",
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { DocumentMutationLog, KubunDB } from '@kubun/db';
|
|
2
|
-
export type MutationLogEntry = {
|
|
3
|
-
documentID: string;
|
|
4
|
-
sequenceNumber: string;
|
|
5
|
-
mutationJWT: string;
|
|
6
|
-
authorDID: string;
|
|
7
|
-
};
|
|
8
|
-
/**
|
|
9
|
-
* Stores a mutation log entry and maintains ring buffer of last 10 mutations per document
|
|
10
|
-
*/
|
|
11
|
-
export declare function storeMutationLog(db: KubunDB, entry: MutationLogEntry): Promise<void>;
|
|
12
|
-
/** @deprecated Use DocumentMutationLog from @kubun/db instead */
|
|
13
|
-
export type MutationLogRecord = DocumentMutationLog;
|
|
14
|
-
/**
|
|
15
|
-
* Retrieves mutation log entries for a document starting from a given sequence number
|
|
16
|
-
*/
|
|
17
|
-
export declare function getMutationLog(db: KubunDB, documentID: string, fromSequence: number): Promise<Array<DocumentMutationLog>>;
|
package/lib/data/mutation-log.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export async function storeMutationLog(t,o){await t.storeMutationLog(o)}export async function getMutationLog(t,o,n){return t.getMutationLog(o,n)}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { ProcedureHandlers } from '@enkaku/server';
|
|
2
|
-
import type { DocumentProtocol } from '@kubun/protocol';
|
|
3
|
-
import type { CreateHandlersParams } from './types.js';
|
|
4
|
-
export declare function createHandlers(handlersParams: CreateHandlersParams): ProcedureHandlers<DocumentProtocol>;
|
package/lib/handlers/document.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{toB64 as e}from"@enkaku/codec";export function createHandlers(t){let{db:n}=t;return{"document/sync":async t=>{let r=Object.keys(t.param.documents),a=await n.getDocumentStates(r);return{states:r.reduce((n,r)=>{let c=a[r];return null==c?n[r]=null:(t.param.documents[r],n[r]=e(c)),n},{})}}}}
|
package/lib/sync/crdt-merge.d.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { KubunDB } from '@kubun/db';
|
|
2
|
-
import type { Logger } from '@kubun/logger';
|
|
3
|
-
export type CRDTMergeResult = {
|
|
4
|
-
documentID: string;
|
|
5
|
-
success: boolean;
|
|
6
|
-
merged: boolean;
|
|
7
|
-
error?: string;
|
|
8
|
-
};
|
|
9
|
-
export type MergeFullStateParams = {
|
|
10
|
-
db: KubunDB;
|
|
11
|
-
logger: Logger;
|
|
12
|
-
documentID: string;
|
|
13
|
-
fullState: string;
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* Merge an Automerge state received from a peer with the local document state.
|
|
17
|
-
*
|
|
18
|
-
* This handles the "full sync" mode where we receive a complete Automerge document
|
|
19
|
-
* and need to merge it with our local state using CRDT semantics.
|
|
20
|
-
*/
|
|
21
|
-
export declare function mergeFullState(params: MergeFullStateParams): Promise<CRDTMergeResult>;
|
|
22
|
-
/**
|
|
23
|
-
* Check if full sync merge is possible for a document.
|
|
24
|
-
* Full sync requires the document to already exist locally.
|
|
25
|
-
*/
|
|
26
|
-
export declare function canMergeFullState(db: KubunDB, documentID: string): Promise<boolean>;
|
package/lib/sync/crdt-merge.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{automergeWasmBase64 as e}from"@automerge/automerge/automerge.wasm.base64";import*as t from"@automerge/automerge/slim";import{lazy as r}from"@enkaku/async";import{fromB64 as a}from"@enkaku/codec";import{asType as n,createValidator as o}from"@enkaku/schema";import{DocumentID as m}from"@kubun/id";let s=r(()=>t.initializeBase64Wasm(e)),l={};async function i(e,t){return null==l[t]&&(l[t]=e.getDocumentModel(t).then(e=>o({...e.schema,$id:t}))),l[t]}export async function mergeFullState(e){let{db:r,logger:o,documentID:l,fullState:u}=e;try{await s;let e=a(u),c=t.load(e),g=m.fromString(l),d=g.model.toString(),[f,p,y]=await Promise.all([r.getDocument(g),r.getDocumentState(l),i(r,d)]);if(null===f)return o.warn("Cannot apply full sync to non-existent document",{documentID:l}),{documentID:l,success:!1,merged:!1,error:"Document does not exist locally. Use incremental sync with create mutation."};let h=p?t.load(p):t.from(f.data||{}),S=t.merge(h,c),k=n(y,JSON.parse(JSON.stringify(S)));return await r.saveDocument({id:g,existing:f,data:k,state:t.save(S)}),o.info("Merged full state for document",{documentID:l,localHeads:t.getHeads(h).length,receivedHeads:t.getHeads(c).length,mergedHeads:t.getHeads(S).length}),{documentID:l,success:!0,merged:!0}}catch(t){let e=t instanceof Error?t.message:String(t);return o.error("Failed to merge full state",{documentID:l,error:e}),{documentID:l,success:!1,merged:!1,error:e}}}export async function canMergeFullState(e,t){try{let r=m.fromString(t),a=await e.getDocument(r);return null!==a}catch{return!1}}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { KubunDB } from '@kubun/db';
|
|
2
|
-
import type { Logger } from '@kubun/logger';
|
|
3
|
-
import type { DocumentNode } from '@kubun/protocol';
|
|
4
|
-
export type MutationReplayResult = {
|
|
5
|
-
documentID: string;
|
|
6
|
-
success: boolean;
|
|
7
|
-
error?: string;
|
|
8
|
-
};
|
|
9
|
-
export type ReplayMutationsParams = {
|
|
10
|
-
db: KubunDB;
|
|
11
|
-
logger: Logger;
|
|
12
|
-
mutationJWTs: string[];
|
|
13
|
-
/** Skip capturing mutations in local log (useful for avoiding duplicate captures) */
|
|
14
|
-
skipCapture?: boolean;
|
|
15
|
-
};
|
|
16
|
-
/**
|
|
17
|
-
* Replay mutation JWTs received from a peer server.
|
|
18
|
-
* This applies each mutation sequentially and captures them in the local mutation log
|
|
19
|
-
* for onward synchronization with other peers.
|
|
20
|
-
*/
|
|
21
|
-
export declare function replayMutations(params: ReplayMutationsParams): Promise<MutationReplayResult[]>;
|
|
22
|
-
/**
|
|
23
|
-
* Replay a single mutation JWT.
|
|
24
|
-
* Returns the resulting document or throws on error.
|
|
25
|
-
*/
|
|
26
|
-
export declare function replaySingleMutation(params: {
|
|
27
|
-
db: KubunDB;
|
|
28
|
-
logger: Logger;
|
|
29
|
-
mutationJWT: string;
|
|
30
|
-
skipCapture?: boolean;
|
|
31
|
-
}): Promise<DocumentNode>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{asType as t,createValidator as a}from"@enkaku/schema";import{verifyToken as o}from"@enkaku/token";import{applyMutation as e}from"@kubun/mutation";import{documentMutation as r}from"@kubun/protocol";import{captureMutation as i}from"../data/mutation-capture.js";let u=a(r);export async function replayMutations(a){let{db:r,logger:n,mutationJWTs:s,skipCapture:c=!1}=a,d=[],m={db:r,validators:{},accessChecker:void 0};for(let a of s)try{let s=await o(a),p=t(u,s.payload),l=await e(m,p);c||await i({db:r,documentID:l.id,mutationPayload:a,authorDID:p.iss}),d.push({documentID:l.id,success:!0}),n.debug("Replayed mutation",{documentID:l.id,type:p.typ,author:p.iss})}catch(a){let t=a instanceof Error?a.message:String(a);n.error("Failed to replay mutation",{error:t}),d.push({documentID:"unknown",success:!1,error:t})}return d}export async function replaySingleMutation(a){let{db:r,logger:n,mutationJWT:s,skipCapture:c=!1}=a,d=t(u,(await o(s)).payload),m=await e({db:r,validators:{},accessChecker:void 0},d);return c||await i({db:r,documentID:m.id,mutationPayload:s,authorDID:d.iss}),n.debug("Replayed single mutation",{documentID:m.id,type:d.typ,author:d.iss}),m}
|