@holochain-syn/client 0.0.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.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # @holochain-syn/client
2
+
3
+ A simple wrapper class that implements the interface for the `syn` zome.
4
+
5
+ ## Usage
6
+
7
+ ```js
8
+ import { HolochainClient, CellClient } from '@holochain-open-dev/cell-client';
9
+ import { SynClient } from '@holochain-syn/client';
10
+
11
+ const hcClient = await HolochainClient.connect(url, 'syn');
12
+ const cellData = hcClient.cellDataByRoleId('syn');
13
+ const cellClient = hcClient.forCell(cellData);
14
+
15
+ const client = new SynClient(cellClient, signal =>
16
+ handleSignal(this.#workspace, signal)
17
+ );
18
+
19
+ const sessionInfo = await client.newSession();
20
+ ```
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@holochain-syn/client",
3
+ "version": "0.0.1",
4
+ "main": "./dist/index.js",
5
+ "module": "./dist/index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "build:watch": "run-singleton \"tsc -w --preserveWatchOutput\""
10
+ },
11
+ "devDependencies": {
12
+ "@types/lodash-es": "^4.17.4",
13
+ "gh-pages": "^3.2.3",
14
+ "rimraf": "^3.0.2",
15
+ "run-singleton-cli": "^0.0.5",
16
+ "typescript": "^4.2.4"
17
+ },
18
+ "dependencies": {
19
+ "@holochain-open-dev/cell-client": "^0.3.2",
20
+ "@holochain-open-dev/core-types": "^0.2.0",
21
+ "@holochain/client": "^0.3.2",
22
+ "@msgpack/msgpack": "^2.7.0",
23
+ "lodash-es": "^4.17.21"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ }
28
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./syn-client";
2
+ export * from "./types/signal";
3
+ export * from "./types/commit";
4
+ export * from "./types/change";
5
+ export * from "./types/folks";
6
+ export * from "./types/heartbeat";
7
+ export * from "./types/session";
8
+ export * from "./types/sync";
@@ -0,0 +1,227 @@
1
+ import type { CellClient } from '@holochain-open-dev/cell-client';
2
+ import type {
3
+ EntryHashB64,
4
+ AgentPubKeyB64,
5
+ } from '@holochain-open-dev/core-types';
6
+ import { encode, decode } from '@msgpack/msgpack';
7
+ import isEqual from 'lodash-es/isEqual';
8
+
9
+ import type {
10
+ ChangeBundle,
11
+ SendChangeInput,
12
+ SendChangeRequestInput,
13
+ } from './types/change';
14
+ import type { Commit, CommitInput, Content } from './types/commit';
15
+ import type { SendFolkLoreInput } from './types/folks';
16
+ import type { SendHeartbeatInput } from './types/heartbeat';
17
+ import type {
18
+ CloseSessionInput,
19
+ NewSessionInput,
20
+ NotifyLeaveSessionInput,
21
+ Session,
22
+ SessionInfo,
23
+ } from './types/session';
24
+ import type { SendSyncRequestInput, SendSyncResponseInput } from './types/sync';
25
+ import { allMessageTypes, SynSignal } from './types/signal';
26
+ import { deepDecodeUint8Arrays } from './utils';
27
+
28
+ export class SynClient {
29
+ unsubscribe: () => void = () => {};
30
+
31
+ constructor(
32
+ public cellClient: CellClient,
33
+ protected handleSignal: (synSignal: SynSignal) => void,
34
+ protected zomeName = 'syn'
35
+ ) {
36
+ const { unsubscribe } = cellClient.addSignalHandler(signal => {
37
+ console.log(signal);
38
+ if (
39
+ isEqual(cellClient.cellId, signal.data.cellId) &&
40
+ signal.data.payload.message &&
41
+ allMessageTypes.includes(signal.data.payload.message.type)
42
+ ) {
43
+ handleSignal(deepDecodeUint8Arrays(signal.data.payload));
44
+ }
45
+ });
46
+
47
+ this.unsubscribe = unsubscribe;
48
+ }
49
+
50
+ public close() {
51
+ return this.unsubscribe();
52
+ }
53
+
54
+ /** Session */
55
+ public async getSession(sessionHash: EntryHashB64): Promise<Session> {
56
+ return this.callZome('get_session', sessionHash);
57
+ }
58
+
59
+ public async newSession(
60
+ newSessionInput: NewSessionInput
61
+ ): Promise<SessionInfo> {
62
+ return this.callZome('new_session', newSessionInput);
63
+ }
64
+
65
+ public async closeSession(input: CloseSessionInput): Promise<void> {
66
+ return this.callZome('close_session', input);
67
+ }
68
+
69
+ public getSessions(): Promise<Record<string, Session>> {
70
+ return this.callZome('get_sessions', null);
71
+ }
72
+
73
+ public notifyLeaveSession(input: NotifyLeaveSessionInput): Promise<void> {
74
+ return this.callZome('notify_leave_session', input);
75
+ }
76
+
77
+ /** Content */
78
+ public putSnapshot(content: Content): Promise<EntryHashB64> {
79
+ return this.callZome('put_snapshot', encode(content));
80
+ }
81
+
82
+ public async getSnapshot(
83
+ snapshotHash: EntryHashB64
84
+ ): Promise<Content | undefined> {
85
+ const content = await this.callZome('get_snapshot', snapshotHash);
86
+ if (!content) return content;
87
+ return decode(content);
88
+ }
89
+
90
+ public hashSnapshot(content: Content): Promise<EntryHashB64> {
91
+ return this.callZome('hash_snapshot', encode(content));
92
+ }
93
+
94
+ /** Commits */
95
+ public commitChanges(commitInput: CommitInput): Promise<EntryHashB64> {
96
+ const commit = {
97
+ ...commitInput,
98
+ commit: {
99
+ ...commitInput.commit,
100
+ changes: this.encodeChangeBundle(commitInput.commit.changes),
101
+ },
102
+ };
103
+
104
+ return this.callZome('commit_changes', commit);
105
+ }
106
+
107
+ public async getCommit(commitHash: EntryHashB64): Promise<Commit> {
108
+ const commit = await this.callZome('get_commit', commitHash);
109
+ return this.decodeCommit(commit);
110
+ }
111
+
112
+ public async getAllCommits(): Promise<Record<string, Commit>> {
113
+ const commits = await this.callZome('get_all_commits', null);
114
+
115
+ for (const key of Object.keys(commits)) {
116
+ commits[key] = this.decodeCommit(commits[key]);
117
+ }
118
+
119
+ return commits;
120
+ }
121
+
122
+ /** Folks */
123
+
124
+ public getFolks(): Promise<Array<AgentPubKeyB64>> {
125
+ return this.callZome('get_folks', null);
126
+ }
127
+
128
+ public sendFolkLore(sendFolkLoreInput: SendFolkLoreInput): Promise<void> {
129
+ return this.callZome('send_folk_lore', sendFolkLoreInput);
130
+ }
131
+
132
+ /** Sync */
133
+ public sendSyncRequest(
134
+ syncRequestInput: SendSyncRequestInput
135
+ ): Promise<void> {
136
+ return this.callZome('send_sync_request', syncRequestInput);
137
+ }
138
+
139
+ public sendSyncResponse(
140
+ syncResponseInput: SendSyncResponseInput
141
+ ): Promise<void> {
142
+ const input = {
143
+ ...syncResponseInput,
144
+ };
145
+
146
+ if (input.state.folkMissedLastCommit) {
147
+ input.state.folkMissedLastCommit.commit = this.encodeCommit(
148
+ input.state.folkMissedLastCommit.commit
149
+ );
150
+ input.state.folkMissedLastCommit.commitInitialSnapshot = encode(
151
+ input.state.folkMissedLastCommit.commitInitialSnapshot
152
+ );
153
+ }
154
+ if (input.state.uncommittedChanges) {
155
+ input.state.uncommittedChanges = this.encodeChangeBundle(
156
+ syncResponseInput.state.uncommittedChanges
157
+ );
158
+ }
159
+
160
+ return this.callZome('send_sync_response', input);
161
+ }
162
+
163
+ /** Changes */
164
+ public sendChangeRequest(
165
+ changeRequestInput: SendChangeRequestInput
166
+ ): Promise<void> {
167
+ const input = { ...changeRequestInput };
168
+ if (input.deltaChanges) {
169
+ input.deltaChanges = {
170
+ ...input.deltaChanges,
171
+ deltas: input.deltaChanges.deltas.map(d => encode(d)),
172
+ };
173
+ }
174
+
175
+ return this.callZome('send_change_request', input);
176
+ }
177
+
178
+ public sendChange(sendChangeInput: SendChangeInput): Promise<void> {
179
+ const input = {
180
+ ...sendChangeInput,
181
+ };
182
+ if (input.deltaChanges) {
183
+ input.deltaChanges = this.encodeChangeBundle(input.deltaChanges);
184
+ }
185
+ return this.callZome('send_change', input);
186
+ }
187
+
188
+ /** Heartbeat */
189
+ public sendHeartbeat(heartbeatInput: SendHeartbeatInput): Promise<void> {
190
+ return this.callZome('send_heartbeat', heartbeatInput);
191
+ }
192
+
193
+ /** Helpers */
194
+ private async callZome(fnName: string, payload: any): Promise<any> {
195
+ return this.cellClient.callZome(this.zomeName, fnName, payload);
196
+ }
197
+
198
+ private encodeCommit(commit: Commit) {
199
+ return {
200
+ ...commit,
201
+ changes: this.encodeChangeBundle(commit.changes),
202
+ };
203
+ }
204
+
205
+ private encodeChangeBundle(changes: ChangeBundle) {
206
+ return {
207
+ ...changes,
208
+ deltas: changes.deltas.map(d => ({
209
+ author: d.author,
210
+ delta: encode(d.delta),
211
+ })),
212
+ };
213
+ }
214
+
215
+ private decodeCommit(commit: any): Commit {
216
+ return {
217
+ ...commit,
218
+ changes: {
219
+ authors: commit.changes.authors,
220
+ deltas: commit.changes.deltas.map(d => ({
221
+ author: d.author,
222
+ delta: decode(d.delta),
223
+ })),
224
+ },
225
+ };
226
+ }
227
+ }
@@ -0,0 +1,66 @@
1
+ import type {
2
+ AgentPubKeyB64,
3
+ EntryHashB64,
4
+ Dictionary,
5
+ } from '@holochain-open-dev/core-types';
6
+
7
+ export type Delta = any;
8
+
9
+ export interface FolkChanges {
10
+ atFolkIndex: number;
11
+ commitChanges: Array<number>;
12
+ }
13
+
14
+ export interface AuthoredDelta {
15
+ author: AgentPubKeyB64;
16
+ delta: Delta;
17
+ }
18
+
19
+ export interface ChangeBundle {
20
+ // Indexed by commit index
21
+ deltas: Array<AuthoredDelta>;
22
+ // AgentPubKeyB64 -> folkIndex -> sessionIndex
23
+ authors: Dictionary<FolkChanges>;
24
+ }
25
+
26
+ // From folk to scribe
27
+ export interface SendChangeRequestInput {
28
+ sessionHash: EntryHashB64;
29
+ scribe: AgentPubKeyB64;
30
+ lastDeltaSeen: LastDeltaSeen;
31
+
32
+ deltaChanges: DeltaChanges;
33
+ }
34
+
35
+ export interface DeltaChanges {
36
+ atFolkIndex: number;
37
+ deltas: Array<Delta>;
38
+ }
39
+
40
+ export interface LastDeltaSeen {
41
+ commitHash: EntryHashB64 | undefined;
42
+ deltaIndexInCommit: number;
43
+ }
44
+
45
+ export interface ChangeRequest {
46
+ folk: AgentPubKeyB64;
47
+ scribe: AgentPubKeyB64;
48
+
49
+ lastDeltaSeen: LastDeltaSeen;
50
+
51
+ deltaChanges: DeltaChanges;
52
+ }
53
+
54
+ export interface SendChangeInput {
55
+ participants: Array<AgentPubKeyB64>;
56
+ sessionHash: EntryHashB64;
57
+ lastDeltaSeen: LastDeltaSeen;
58
+
59
+ deltaChanges: ChangeBundle;
60
+ }
61
+
62
+ export interface ChangeNotice {
63
+ lastDeltaSeen: LastDeltaSeen;
64
+
65
+ deltaChanges: ChangeBundle;
66
+ }
@@ -0,0 +1,41 @@
1
+ import type {
2
+ AgentPubKeyB64,
3
+ EntryHashB64,
4
+ HeaderHashB64,
5
+ } from '@holochain-open-dev/core-types';
6
+ import type { ChangeBundle } from './change';
7
+
8
+ export interface CommitInput {
9
+ sessionHash: EntryHashB64;
10
+
11
+ commit: Commit;
12
+
13
+ participants: AgentPubKeyB64[];
14
+ }
15
+
16
+ export interface Commit {
17
+ changes: ChangeBundle;
18
+
19
+ createdAt: number;
20
+
21
+ previousCommitHashes: Array<EntryHashB64>;
22
+
23
+ previousContentHash: EntryHashB64;
24
+ newContentHash: EntryHashB64;
25
+ meta: ChangeMeta;
26
+ }
27
+
28
+ export interface CommitNotice {
29
+ commitHash: HeaderHashB64;
30
+ committedDeltasCount: number;
31
+ previousContentHash: EntryHashB64;
32
+ newContentHash: EntryHashB64;
33
+ meta: ChangeMeta;
34
+ }
35
+
36
+ export interface ChangeMeta {
37
+ witnesses: AgentPubKeyB64[];
38
+ appSpecific: Content;
39
+ }
40
+
41
+ export type Content = any;
@@ -0,0 +1,17 @@
1
+ import type {
2
+ AgentPubKeyB64,
3
+ Dictionary,
4
+ EntryHashB64,
5
+ } from '@holochain-open-dev/core-types';
6
+
7
+ export interface SendFolkLoreInput {
8
+ sessionHash: EntryHashB64;
9
+ participants: Array<AgentPubKeyB64>;
10
+ folkLore: FolkLore;
11
+ }
12
+
13
+ export interface FolkInfo {
14
+ lastSeen: number;
15
+ }
16
+
17
+ export type FolkLore = Dictionary<FolkInfo>;
@@ -0,0 +1,15 @@
1
+ import type {
2
+ AgentPubKeyB64,
3
+ EntryHashB64,
4
+ } from "@holochain-open-dev/core-types";
5
+
6
+ export interface SendHeartbeatInput {
7
+ sessionHash: EntryHashB64;
8
+ scribe: AgentPubKeyB64;
9
+ data: String;
10
+ }
11
+
12
+ export interface Heartbeat {
13
+ fromFolk: AgentPubKeyB64;
14
+ data: string;
15
+ }
@@ -0,0 +1,29 @@
1
+ import type {
2
+ AgentPubKeyB64,
3
+ EntryHashB64,
4
+ } from '@holochain-open-dev/core-types';
5
+
6
+ export interface Session {
7
+ initialCommitHash: EntryHashB64 | undefined;
8
+ scribe: AgentPubKeyB64;
9
+ createdAt: number;
10
+ }
11
+
12
+ export interface SessionInfo {
13
+ sessionHash: EntryHashB64;
14
+ session: Session;
15
+ }
16
+
17
+ export interface NewSessionInput {
18
+ initialCommitHash: EntryHashB64 | undefined;
19
+ }
20
+
21
+ export interface CloseSessionInput {
22
+ sessionHash: EntryHashB64;
23
+ participants: AgentPubKeyB64[];
24
+ }
25
+
26
+ export interface NotifyLeaveSessionInput {
27
+ sessionHash: EntryHashB64;
28
+ scribe: AgentPubKeyB64;
29
+ }
@@ -0,0 +1,52 @@
1
+ import type { CommitNotice } from './commit';
2
+
3
+ import type { ChangeNotice, ChangeRequest } from './change';
4
+ import type { FolkLore } from './folks';
5
+ import type { Heartbeat } from './heartbeat';
6
+ import type { RequestSyncInput, StateForSync } from './sync';
7
+ import type { AgentPubKeyB64 } from '@holochain-open-dev/core-types';
8
+
9
+ export enum SynMessageType {
10
+ SyncReq = 'SyncReq',
11
+ SyncResp = 'SyncResp',
12
+ ChangeReq = 'ChangeReq',
13
+ ChangeNotice = 'ChangeNotice',
14
+ CommitNotice = 'CommitNotice',
15
+ Heartbeat = 'Heartbeat',
16
+ FolkLore = 'FolkLore',
17
+ SessionClosed = 'SessionClosed',
18
+ LeaveSessionNotice = 'LeaveSessionNotice',
19
+ }
20
+
21
+ export const allMessageTypes = [
22
+ SynMessageType.SyncReq,
23
+ SynMessageType.SyncResp,
24
+ SynMessageType.ChangeReq,
25
+ SynMessageType.ChangeNotice,
26
+ SynMessageType.CommitNotice,
27
+ SynMessageType.Heartbeat,
28
+ SynMessageType.FolkLore,
29
+ SynMessageType.SessionClosed,
30
+ SynMessageType.LeaveSessionNotice,
31
+ ];
32
+
33
+ export type SynSignal = {
34
+ sessionHash: string;
35
+ message: SynMessage;
36
+ };
37
+
38
+ export interface MessageBody<TYPE, PAYLOAD> {
39
+ type: TYPE;
40
+ payload: PAYLOAD;
41
+ }
42
+
43
+ export type SynMessage =
44
+ | MessageBody<SynMessageType.SyncReq, RequestSyncInput>
45
+ | MessageBody<SynMessageType.SyncResp, StateForSync>
46
+ | MessageBody<SynMessageType.ChangeReq, ChangeRequest>
47
+ | MessageBody<SynMessageType.ChangeNotice, ChangeNotice>
48
+ | MessageBody<SynMessageType.CommitNotice, CommitNotice>
49
+ | MessageBody<SynMessageType.Heartbeat, Heartbeat>
50
+ | MessageBody<SynMessageType.FolkLore, FolkLore>
51
+ | MessageBody<SynMessageType.LeaveSessionNotice, AgentPubKeyB64>
52
+ | MessageBody<SynMessageType.SessionClosed, void>;
@@ -0,0 +1,42 @@
1
+ import type { ChangeBundle, LastDeltaSeen } from './change';
2
+ import type {
3
+ EntryHashB64,
4
+ AgentPubKeyB64,
5
+ } from '@holochain-open-dev/core-types';
6
+ import type { Commit, Content } from './commit';
7
+
8
+ // Sent by the folk
9
+ export interface SendSyncRequestInput {
10
+ sessionHash: EntryHashB64;
11
+ scribe: AgentPubKeyB64;
12
+ lastDeltaSeen: LastDeltaSeen | undefined;
13
+ }
14
+
15
+ // Received by the scribe
16
+ export interface RequestSyncInput {
17
+ folk: AgentPubKeyB64;
18
+
19
+ scribe: AgentPubKeyB64;
20
+ lastDeltaSeen: LastDeltaSeen;
21
+ }
22
+
23
+ export interface MissedCommit {
24
+ commitHash: EntryHashB64;
25
+ commit: Commit;
26
+ commitInitialSnapshot: Content;
27
+ }
28
+
29
+ export interface StateForSync {
30
+ // Will contain the newer commits since `lastSessionIndexSeen`
31
+ folkMissedLastCommit: MissedCommit | undefined;
32
+
33
+ uncommittedChanges: ChangeBundle;
34
+
35
+ //currentContentHash: EntryHashB64;
36
+ }
37
+
38
+ export interface SendSyncResponseInput {
39
+ sessionHash: EntryHashB64;
40
+ participant: AgentPubKeyB64;
41
+ state: StateForSync;
42
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { decode } from "@msgpack/msgpack";
2
+
3
+ export function deepDecodeUint8Arrays(object: any): any {
4
+ if (object === undefined || object === null) return object;
5
+ if (object instanceof Uint8Array) return decode(object);
6
+
7
+ if (typeof object !== "object") return object;
8
+
9
+ if (Array.isArray(object)) return object.map(deepDecodeUint8Arrays);
10
+
11
+ const obj = {};
12
+
13
+ for (const key of Object.keys(object)) {
14
+ obj[key] = deepDecodeUint8Arrays(object[key]);
15
+ }
16
+ return obj;
17
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "skipLibCheck": true,
5
+ "skipDefaultLibCheck": true,
6
+ "outDir": "dist",
7
+ "declarationDir": "dist",
8
+ "declaration": true
9
+ },
10
+ "include": [
11
+ "./src/**/*"
12
+ ],
13
+ "exclude": [
14
+ "node_modules/**",
15
+ "**/node_modules/**",
16
+ "**/dist/**"
17
+ ]
18
+ }