Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
target
data
.claude
.claude
**/node_modules
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 64 additions & 0 deletions clients/typescript/cli/commands/unreads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Did } from "../../src/codec.js";
import {
createClient,
parseGlobalOptions,
outputJson,
outputError,
} from "../utils.js";

export async function getUnreads(args: string[]) {
if (args.length < 1) {
throw new Error("Usage: leaf unreads <stream-did>");
}

const streamDid = args[0]! as Did;
const options = parseGlobalOptions(args);

const client = await createClient(options);

try {
const unreads = await client.getUnreads(streamDid);

outputJson({
success: true,
stream_id: streamDid,
unreads,
});
} catch (error) {
outputError(error instanceof Error ? error.message : String(error));
throw error;
} finally {
client.disconnect();
}
}

export async function markAsRead(args: string[]) {
if (args.length < 1) {
throw new Error(
"Usage: leaf mark-read <stream-did> [room-id] [last-read-idx]",
);
}

const streamDid = args[0]! as Did;
const roomId = args[1];
const lastReadIdx = args[2] ? parseInt(args[2], 10) : undefined;
const options = parseGlobalOptions(args);

const client = await createClient(options);

try {
const success = await client.markAsRead(streamDid, roomId, lastReadIdx);

outputJson({
success,
stream_id: streamDid,
room_id: roomId,
last_read_idx: lastReadIdx,
});
} catch (error) {
outputError(error instanceof Error ? error.message : String(error));
throw error;
} finally {
client.disconnect();
}
}
12 changes: 12 additions & 0 deletions clients/typescript/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { query } from "./commands/query.js";
import { sendEvents } from "./commands/send-events.js";
import { createStream } from "./commands/create-stream.js";
import { streamInfo } from "./commands/stream-info.js";
import { getUnreads, markAsRead } from "./commands/unreads.js";

const HELP_TEXT = `
Leaf CLI - Testing tool for Leaf server
Expand All @@ -16,6 +17,8 @@ Commands:
send-events <stream-did> <file> Send events to a stream from JSON file
create-stream <module-cid> Create a new stream from genesis JSON
stream-info <stream-did> Get stream information
unreads <stream-did> Get unread counts for a user
mark-read <stream-did> [room-id] Mark items as read (all rooms or specific room)

Global Options:
--url <url> Leaf server URL (default: http://localhost:5530 or LEAF_URL env var)
Expand All @@ -33,6 +36,9 @@ Examples:
leaf send-events abc123 events.json
leaf create-stream a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6
leaf stream-info abc123
leaf unreads abc123
leaf mark-read abc123
leaf mark-read abc123 room123 100

Environment Variables:
LEAF_URL Default Leaf server URL
Expand Down Expand Up @@ -64,6 +70,12 @@ async function main() {
case "stream-info":
await streamInfo(commandArgs);
break;
case "unreads":
await getUnreads(commandArgs);
break;
case "mark-read":
await markAsRead(commandArgs);
break;
default:
console.error(`Unknown command: ${command}`);
console.error("Run 'leaf --help' for usage information");
Expand Down
30 changes: 29 additions & 1 deletion clients/typescript/src/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,32 @@ export type StreamStateEventBatchResp = Result<void>;
export type StreamClearStateArgs = {
streamDid: Did;
};
export type StreamClearStateResp = Result<void>;
export type StreamClearStateResp = Result<void>;

// ============================================================================
// Unreads tracking types
// ============================================================================

export type UnreadsGetArgs = {
streamDid: Did;
};

export type UnreadsGetItem = {
roomId: string;
unreadCount: number;
mentionCount: number;
};

export type UnreadsGetResp = Result<{
unreads: UnreadsGetItem[];
}>;

export type UnreadsMarkReadArgs = {
streamDid: Did;
roomId?: string;
lastReadIdx?: number;
};

export type UnreadsMarkReadResp = Result<{
success: boolean;
}>;
52 changes: 49 additions & 3 deletions clients/typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,18 @@ import {
StreamUpdateModuleResp,
SubscribeEventsResp,
SubscriptionId,
UnreadsGetArgs,
UnreadsGetItem,
UnreadsGetResp,
UnreadsMarkReadArgs,
UnreadsMarkReadResp,
} from "./codec.js";

export * from "./codec.js";

type SocketIoBuffer = Buffer | ArrayBuffer;

async function createDaslCid(bytes: Uint8Array): Promise<Cid> {
async function createDaslCid(bytes: Uint8Array<ArrayBuffer>): Promise<Cid> {
return createCid(0x71, bytes);
}

Expand Down Expand Up @@ -256,7 +261,10 @@ export class LeafClient {
}
}

async sendStateEvents(streamDid: string, events: Uint8Array[]): Promise<void> {
async sendStateEvents(
streamDid: string,
events: Uint8Array[],
): Promise<void> {
const data: Uint8Array = await this.socket.emitWithAck(
"stream/state_event_batch",
toBinary(
Expand All @@ -275,7 +283,9 @@ export class LeafClient {
async clearState(streamDid: string): Promise<void> {
const data: Uint8Array = await this.socket.emitWithAck(
"stream/clear_state",
toBinary(encode({ streamDid: streamDid as Did } satisfies StreamClearStateArgs)),
toBinary(
encode({ streamDid: streamDid as Did } satisfies StreamClearStateArgs),
),
);
const resp: StreamClearStateResp = decode(fromBinary(data));
if ("Err" in resp) {
Expand Down Expand Up @@ -357,6 +367,42 @@ export class LeafClient {
throw new Error(resp.Err);
}
}

async getUnreads(streamDid: string): Promise<UnreadsGetItem[]> {
const data: Uint8Array = await this.socket.emitWithAck(
"unreads/get",
toBinary(
encode({ streamDid: streamDid as Did } satisfies UnreadsGetArgs),
),
);
const resp: UnreadsGetResp = decode(fromBinary(data));
if ("Err" in resp) {
throw new Error(resp.Err);
}
return resp.Ok.unreads;
}

async markAsRead(
streamDid: string,
roomId?: string,
lastReadIdx?: number,
): Promise<boolean> {
const data: Uint8Array = await this.socket.emitWithAck(
"unreads/mark_read",
toBinary(
encode({
streamDid: streamDid as Did,
roomId,
lastReadIdx,
} satisfies UnreadsMarkReadArgs),
),
);
const resp: UnreadsMarkReadResp = decode(fromBinary(data));
if ("Err" in resp) {
throw new Error(resp.Err);
}
return resp.Ok.success;
}
}

function convertBytesWrappers(t: any): any {
Expand Down
4 changes: 2 additions & 2 deletions explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-sql": "^6.10.0",
"@codemirror/theme-one-dark": "^6.1.3",
"@muni-town/leaf-client": "0.1.0-alpha.17",
"@muni-town/leaf-client": "workspace:*",
"svelte-codemirror-editor": "^2.1.0"
}
}
}
Loading