Initial infrastructure and code
This commit is contained in:
parent
f2350fc6b2
commit
6be08230c3
43 changed files with 1988 additions and 10 deletions
7
deno.json
Normal file
7
deno.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"nanoid": "https://deno.land/x/nanoid@v3.0.0/mod.ts",
|
||||||
|
"events": "https://deno.land/x/events@v1.0.0/mod.ts",
|
||||||
|
"typed-emitter": "npm:typed-emitter"
|
||||||
|
}
|
||||||
|
}
|
15
deno.lock
Normal file
15
deno.lock
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"version": "3",
|
||||||
|
"redirects": {
|
||||||
|
"https://deno.land/x/nanoid/mod.ts": "https://deno.land/x/nanoid@v3.0.0/mod.ts"
|
||||||
|
},
|
||||||
|
"remote": {
|
||||||
|
"https://deno.land/x/events@v1.0.0/mod.ts": "3e2655ffa5e86a6ee01022f964b7fdc6152c007106c47b02958e766c6614dbaf",
|
||||||
|
"https://deno.land/x/nanoid@v3.0.0/customAlphabet.ts": "1cfd7cfd2f07ca8d78a7e7855fcc9f59abf01ef2a127484ef94328fadf940ead",
|
||||||
|
"https://deno.land/x/nanoid@v3.0.0/customRandom.ts": "af56e19038c891a4b4ef2be931554c27579bd407ee5bbea5cb64f6ee1347cbe3",
|
||||||
|
"https://deno.land/x/nanoid@v3.0.0/mod.ts": "3ead610e40c58d8fdca21d5da9ec661445a2b82526e19c34d05de5f90be8a1be",
|
||||||
|
"https://deno.land/x/nanoid@v3.0.0/nanoid.ts": "8d119bc89a0f34e7bbe0c2dbdc280d01753e431af553d189663492310a31085d",
|
||||||
|
"https://deno.land/x/nanoid@v3.0.0/random.ts": "4da71d5f72f2bfcc6a4ee79b5d4e72f48dcf4fe4c3835fd5ebab08b9f33cd598",
|
||||||
|
"https://deno.land/x/nanoid@v3.0.0/urlAlphabet.ts": "8b1511deb1ecb23c66202b6000dc10fb68f9a96b5550c6c8cef5009324793431"
|
||||||
|
}
|
||||||
|
}
|
0
dist/src/Connection.d.ts
vendored
Normal file
0
dist/src/Connection.d.ts
vendored
Normal file
1
dist/src/Connection.js
vendored
Normal file
1
dist/src/Connection.js
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"use strict";
|
178
dist/src/api.d.ts
vendored
Normal file
178
dist/src/api.d.ts
vendored
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
export type RoomId = string;
|
||||||
|
export type UserId = string;
|
||||||
|
export type EventId = string;
|
||||||
|
export interface ApiEvent {
|
||||||
|
event_id: EventId;
|
||||||
|
sender: UserId;
|
||||||
|
type: string;
|
||||||
|
content: any;
|
||||||
|
state_key?: string;
|
||||||
|
unsigned?: any;
|
||||||
|
}
|
||||||
|
export type ApiStateEvent = ApiEvent & {
|
||||||
|
state_key: string;
|
||||||
|
};
|
||||||
|
export type ApiDeviceEvent = {
|
||||||
|
sender: UserId;
|
||||||
|
type: string;
|
||||||
|
content: any;
|
||||||
|
};
|
||||||
|
export type ApiEphemeralEvent = {
|
||||||
|
sender: UserId;
|
||||||
|
type: string;
|
||||||
|
content: any;
|
||||||
|
key?: string;
|
||||||
|
persist_until?: number;
|
||||||
|
};
|
||||||
|
export type RoomSubscription = {
|
||||||
|
query: string;
|
||||||
|
};
|
||||||
|
export type ListSubscription = {
|
||||||
|
ranges?: Array<[number, number]>;
|
||||||
|
query?: string;
|
||||||
|
filters?: {
|
||||||
|
spaces?: Array<RoomId | null>;
|
||||||
|
types?: Array<string | null>;
|
||||||
|
purposes?: Array<string | null>;
|
||||||
|
is_invite_knock?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export interface SyncRequest {
|
||||||
|
pos?: string;
|
||||||
|
timeout?: number;
|
||||||
|
txn_id?: string;
|
||||||
|
conn_id?: string;
|
||||||
|
delta_token?: string;
|
||||||
|
queries?: Record<string, null | {
|
||||||
|
required_state?: Array<[string, string]>;
|
||||||
|
timeline?: {
|
||||||
|
limit?: number;
|
||||||
|
types?: Array<string>;
|
||||||
|
not_types?: Array<string>;
|
||||||
|
};
|
||||||
|
ephemeral?: {
|
||||||
|
limit?: number;
|
||||||
|
types?: Array<string>;
|
||||||
|
include_old?: boolean;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
lists?: Record<string, null | ListSubscription>;
|
||||||
|
rooms?: Record<RoomId, null | RoomSubscription>;
|
||||||
|
extensions?: {
|
||||||
|
"m.to_device"?: {
|
||||||
|
enabled: boolean;
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
"m.e2ee"?: {
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
"m.account_data"?: {
|
||||||
|
enabled: boolean;
|
||||||
|
lists: Array<string>;
|
||||||
|
rooms: Array<RoomId>;
|
||||||
|
};
|
||||||
|
"m.presence"?: {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export interface SyncResponseRoom {
|
||||||
|
initial?: boolean;
|
||||||
|
required_state?: Array<ApiStateEvent>;
|
||||||
|
timeline?: Array<ApiEvent>;
|
||||||
|
ephemeral?: Array<ApiEphemeralEvent>;
|
||||||
|
prev_batch?: string;
|
||||||
|
joined_count?: number;
|
||||||
|
invited_count?: number;
|
||||||
|
unreads?: Unreads;
|
||||||
|
}
|
||||||
|
export interface SyncResponse {
|
||||||
|
pos: string;
|
||||||
|
txn_id?: string;
|
||||||
|
delta_token?: string;
|
||||||
|
lists?: Record<string, {
|
||||||
|
count: number;
|
||||||
|
ops: Array<{
|
||||||
|
op: "SYNC";
|
||||||
|
range: [number, number];
|
||||||
|
room_ids: Array<RoomId>;
|
||||||
|
} | {
|
||||||
|
op: "INSERT";
|
||||||
|
index: number;
|
||||||
|
room_id: RoomId;
|
||||||
|
} | {
|
||||||
|
op: "DELETE";
|
||||||
|
index: number;
|
||||||
|
room_ids: Array<RoomId>;
|
||||||
|
} | {
|
||||||
|
op: "INVALIDATE";
|
||||||
|
range: [number, number];
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
rooms?: Record<RoomId, SyncResponseRoom>;
|
||||||
|
extensions?: {
|
||||||
|
"m.to_device"?: {
|
||||||
|
enabled: boolean;
|
||||||
|
events: Array<ApiDeviceEvent>;
|
||||||
|
};
|
||||||
|
"m.account_data"?: {
|
||||||
|
enabled: boolean;
|
||||||
|
lists: Array<string>;
|
||||||
|
rooms: Array<RoomId>;
|
||||||
|
};
|
||||||
|
"m.e2ee"?: {
|
||||||
|
"device_one_time_keys_count": {
|
||||||
|
"signed_curve25519": number;
|
||||||
|
};
|
||||||
|
"device_lists": {
|
||||||
|
"changed": Array<UserId>;
|
||||||
|
"left": Array<UserId>;
|
||||||
|
};
|
||||||
|
"device_unused_fallback_key_types": [
|
||||||
|
"signed_curve25519"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"m.presence"?: {};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export interface Unreads {
|
||||||
|
last_ack?: EventId;
|
||||||
|
mention_user?: number;
|
||||||
|
mention_bulk?: number;
|
||||||
|
notify?: number;
|
||||||
|
messages?: number;
|
||||||
|
}
|
||||||
|
export interface ContextResponse {
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
event: ApiEvent;
|
||||||
|
events_before: Array<ApiEvent>;
|
||||||
|
events_after: Array<ApiEvent>;
|
||||||
|
state: Array<ApiStateEvent>;
|
||||||
|
}
|
||||||
|
export interface MessagesResponse {
|
||||||
|
chunk: Array<ApiEvent>;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
state: Array<ApiStateEvent>;
|
||||||
|
}
|
||||||
|
export interface RelationsResponse {
|
||||||
|
chunk: Array<ApiEvent>;
|
||||||
|
next_batch: string;
|
||||||
|
prev_batch: string;
|
||||||
|
}
|
||||||
|
export interface SendResponse {
|
||||||
|
event_id: EventId;
|
||||||
|
}
|
||||||
|
export type InboxFilter = "default" | "mentions_user" | "mentions_bulk" | "threads_participating" | "threads_interesting" | "include_read";
|
||||||
|
export interface InboxResponse {
|
||||||
|
next_batch?: string;
|
||||||
|
threads?: Array<ApiEvent>;
|
||||||
|
chunk?: Array<{
|
||||||
|
event: ApiEvent;
|
||||||
|
read: boolean;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
export type IncludeThreads = "read" | "ignoring";
|
||||||
|
export interface ThreadsResponse {
|
||||||
|
chunk?: Array<Event>;
|
||||||
|
next_batch?: string;
|
||||||
|
}
|
2
dist/src/api.js
vendored
Normal file
2
dist/src/api.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
// all the types for the api
|
||||||
|
export {};
|
53
dist/src/client.d.ts
vendored
Normal file
53
dist/src/client.d.ts
vendored
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { ApiDeviceEvent, ListSubscription, RoomSubscription } from "./api.js";
|
||||||
|
import { Network } from "./net.js";
|
||||||
|
import { Room } from "./room.js";
|
||||||
|
import { Connection } from "./sync.js";
|
||||||
|
import TypedEmitter from "typed-emitter";
|
||||||
|
interface ClientConfig {
|
||||||
|
baseUrl: string;
|
||||||
|
token: string;
|
||||||
|
userId: string;
|
||||||
|
deviceId: string;
|
||||||
|
}
|
||||||
|
type ClientState = {
|
||||||
|
state: "stop";
|
||||||
|
} | {
|
||||||
|
state: "sync";
|
||||||
|
} | {
|
||||||
|
state: "catchup";
|
||||||
|
} | {
|
||||||
|
state: "error";
|
||||||
|
reason: any;
|
||||||
|
} | {
|
||||||
|
state: "retry";
|
||||||
|
backoff: number;
|
||||||
|
};
|
||||||
|
type ClientEvents = {
|
||||||
|
state: (state: ClientState) => void;
|
||||||
|
list: (name: string, list: RoomList) => void;
|
||||||
|
roomInit: (room: Room) => void;
|
||||||
|
roomDeinit: (room: Room) => void;
|
||||||
|
accountData: (type: string, content: string) => void;
|
||||||
|
toDevice: (event: ApiDeviceEvent) => void;
|
||||||
|
};
|
||||||
|
type RoomList = {
|
||||||
|
count: number;
|
||||||
|
rooms: Array<Room>;
|
||||||
|
};
|
||||||
|
declare const Client_base: new () => TypedEmitter<ClientEvents>;
|
||||||
|
export declare class Client extends Client_base {
|
||||||
|
config: ClientConfig;
|
||||||
|
state: ClientState;
|
||||||
|
net: Network;
|
||||||
|
conn: Connection;
|
||||||
|
lists: Map<string, RoomList>;
|
||||||
|
rooms: Map<string, Room>;
|
||||||
|
constructor(config: ClientConfig);
|
||||||
|
start(): void;
|
||||||
|
stop(): void;
|
||||||
|
listSubscribe(name: string, subscription: ListSubscription): void;
|
||||||
|
listUnsubscribe(name: string): void;
|
||||||
|
roomSubscribe(roomId: string, subscription: RoomSubscription): void;
|
||||||
|
roomUnsubscribe(roomId: string): void;
|
||||||
|
}
|
||||||
|
export {};
|
85
dist/src/client.js
vendored
Normal file
85
dist/src/client.js
vendored
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
// The "main" class that all interaction can be done through.
|
||||||
|
import { Network } from "./net.js";
|
||||||
|
import { Connection } from "./sync.js";
|
||||||
|
import EventEmitter from "events";
|
||||||
|
export class Client extends EventEmitter {
|
||||||
|
constructor(config) {
|
||||||
|
super();
|
||||||
|
Object.defineProperty(this, "config", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: config
|
||||||
|
});
|
||||||
|
// these shouldn't be public, but ah typescript
|
||||||
|
Object.defineProperty(this, "state", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: { state: "stop" }
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "net", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "conn", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "lists", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: new Map()
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "rooms", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: new Map()
|
||||||
|
});
|
||||||
|
this.net = new Network(this, {
|
||||||
|
verbose: true,
|
||||||
|
});
|
||||||
|
this.conn = new Connection(this);
|
||||||
|
}
|
||||||
|
// Start receiving events from /sync.
|
||||||
|
// WARN: if you lose the reference to Client, the poller will leak
|
||||||
|
start() {
|
||||||
|
this.state = { state: "sync" };
|
||||||
|
(async () => {
|
||||||
|
while (this.state.state === "sync") {
|
||||||
|
try {
|
||||||
|
await this.conn.sync();
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
this.state = { state: "error", reason: err };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
// Stop receiving events from /sync.
|
||||||
|
stop() {
|
||||||
|
this.conn.abort();
|
||||||
|
this.conn = new Connection(this);
|
||||||
|
this.state = { state: "stop" };
|
||||||
|
}
|
||||||
|
listSubscribe(name, subscription) {
|
||||||
|
this.lists.set(name, { count: 0, rooms: [] });
|
||||||
|
this.conn.listSubscribe(name, subscription);
|
||||||
|
}
|
||||||
|
listUnsubscribe(name) {
|
||||||
|
this.lists.delete(name);
|
||||||
|
this.conn.listUnsubscribe(name);
|
||||||
|
}
|
||||||
|
roomSubscribe(roomId, subscription) {
|
||||||
|
this.conn.roomSubscribe(roomId, subscription);
|
||||||
|
}
|
||||||
|
roomUnsubscribe(roomId) {
|
||||||
|
this.conn.roomUnsubscribe(roomId);
|
||||||
|
}
|
||||||
|
}
|
18
dist/src/event.d.ts
vendored
Normal file
18
dist/src/event.d.ts
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { Client } from "./client.js";
|
||||||
|
import { ApiEvent, ApiStateEvent, EventId, UserId } from "./api.js";
|
||||||
|
import { Room } from "./room.js";
|
||||||
|
export declare class Event {
|
||||||
|
room: Room;
|
||||||
|
client: Client;
|
||||||
|
content: any;
|
||||||
|
id: EventId;
|
||||||
|
sender: UserId;
|
||||||
|
stateKey?: string;
|
||||||
|
type: string;
|
||||||
|
unsigned: any;
|
||||||
|
constructor(room: Room, json: ApiEvent);
|
||||||
|
}
|
||||||
|
export declare class StateEvent extends Event {
|
||||||
|
stateKey: string;
|
||||||
|
constructor(room: Room, json: ApiStateEvent);
|
||||||
|
}
|
70
dist/src/event.js
vendored
Normal file
70
dist/src/event.js
vendored
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
export class Event {
|
||||||
|
constructor(room, json) {
|
||||||
|
Object.defineProperty(this, "room", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: room
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "client", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: this.room.client
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "content", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "id", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "sender", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "stateKey", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "type", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "unsigned", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
this.content = json.content;
|
||||||
|
this.id = json.event_id;
|
||||||
|
this.sender = json.sender;
|
||||||
|
this.stateKey = json.state_key;
|
||||||
|
this.type = json.type;
|
||||||
|
this.unsigned = json.unsigned;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class StateEvent extends Event {
|
||||||
|
constructor(room, json) {
|
||||||
|
super(room, json);
|
||||||
|
Object.defineProperty(this, "stateKey", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
this.stateKey = json.state_key;
|
||||||
|
}
|
||||||
|
}
|
2
dist/src/index.d.ts
vendored
2
dist/src/index.d.ts
vendored
|
@ -1 +1 @@
|
||||||
export declare function hello(name: string): void;
|
export { Client } from "./client.js";
|
||||||
|
|
4
dist/src/index.js
vendored
4
dist/src/index.js
vendored
|
@ -1,3 +1 @@
|
||||||
export function hello(name) {
|
export { Client } from "./client.js";
|
||||||
console.log(`hello ${name}!`);
|
|
||||||
}
|
|
||||||
|
|
46
dist/src/net.d.ts
vendored
Normal file
46
dist/src/net.d.ts
vendored
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import * as t from "./api.js";
|
||||||
|
import { Client } from "./client.js";
|
||||||
|
interface NetworkConfig {
|
||||||
|
fetchImpl?: typeof fetch;
|
||||||
|
verbose?: boolean;
|
||||||
|
}
|
||||||
|
export declare class Network {
|
||||||
|
private client;
|
||||||
|
private config;
|
||||||
|
constructor(client: Client, config: NetworkConfig);
|
||||||
|
private fetch;
|
||||||
|
sync(options: t.SyncRequest, signal: AbortSignal): Promise<t.SyncResponse>;
|
||||||
|
fetchEvent(roomId: t.RoomId, eventId: t.EventId): Promise<t.ApiEvent>;
|
||||||
|
fetchContext(roomId: t.RoomId, eventId: t.EventId, limit?: number): Promise<t.ContextResponse>;
|
||||||
|
fetchMessages(opts: {
|
||||||
|
roomId: t.RoomId;
|
||||||
|
dir: "b" | "f";
|
||||||
|
limit?: number;
|
||||||
|
from: string;
|
||||||
|
to?: string;
|
||||||
|
}): Promise<t.MessagesResponse>;
|
||||||
|
fetchRelations(roomId: t.RoomId, eventId: t.EventId, opts: {
|
||||||
|
relType?: string;
|
||||||
|
eventType?: string;
|
||||||
|
limit?: string;
|
||||||
|
dir: "b" | "f";
|
||||||
|
from?: string;
|
||||||
|
to?: string;
|
||||||
|
}): Promise<t.RelationsResponse>;
|
||||||
|
fetchThreads(opts: {
|
||||||
|
from?: string;
|
||||||
|
limit?: number;
|
||||||
|
roomIds?: Array<t.RoomId>;
|
||||||
|
watching: boolean;
|
||||||
|
include: Array<t.IncludeThreads>;
|
||||||
|
}): Promise<t.ThreadsResponse>;
|
||||||
|
fetchInbox(opts: {
|
||||||
|
roomIds?: Array<t.RoomId>;
|
||||||
|
from?: string;
|
||||||
|
filter?: t.InboxFilter;
|
||||||
|
limit?: number;
|
||||||
|
}): Promise<t.InboxResponse>;
|
||||||
|
sendEvent(roomId: t.RoomId, type: string, txnId: string, content: any): Promise<t.SendResponse>;
|
||||||
|
sendState(roomId: t.RoomId, type: string, stateKey: string, content: any): Promise<t.SendResponse>;
|
||||||
|
}
|
||||||
|
export {};
|
128
dist/src/net.js
vendored
Normal file
128
dist/src/net.js
vendored
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
const e = (s) => encodeURIComponent(s);
|
||||||
|
function getMethodColor(method) {
|
||||||
|
// i love using the more obscure css colors
|
||||||
|
switch (method) {
|
||||||
|
case "GET": return "deepskyblue";
|
||||||
|
case "POST": return "springgreen";
|
||||||
|
case "PUT": return "lightsalmon";
|
||||||
|
case "PATCH": return "sandybrown";
|
||||||
|
case "DELETE": return "salmon";
|
||||||
|
default: return "orchid";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class Network {
|
||||||
|
constructor(client, config) {
|
||||||
|
Object.defineProperty(this, "client", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: client
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "config", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: config
|
||||||
|
});
|
||||||
|
if (!config.fetchImpl)
|
||||||
|
config.fetchImpl = globalThis.fetch;
|
||||||
|
}
|
||||||
|
async fetch(options) {
|
||||||
|
const url = `${this.client.config.baseUrl}${options.path}`;
|
||||||
|
const isJson = typeof options.body === "object"
|
||||||
|
&& !ArrayBuffer.isView(options.body)
|
||||||
|
&& !(options.body instanceof ReadableStream);
|
||||||
|
if (this.config.verbose) {
|
||||||
|
console.log("%c%s%c %s", getMethodColor(options.method), options.method, "color: initial", options.path);
|
||||||
|
}
|
||||||
|
const req = await this.config.fetchImpl(url, {
|
||||||
|
method: options.method,
|
||||||
|
headers: {
|
||||||
|
"authorization": `Bearer ${this.client.config.token}`,
|
||||||
|
},
|
||||||
|
body: (isJson ? JSON.stringify(options.body) : options.body),
|
||||||
|
...options.extra,
|
||||||
|
});
|
||||||
|
if (!req.ok)
|
||||||
|
throw new Error(`Request failed: ${await req.text()}`);
|
||||||
|
if (options.raw)
|
||||||
|
return req.body;
|
||||||
|
return req.json();
|
||||||
|
}
|
||||||
|
async sync(options, signal) {
|
||||||
|
return this.fetch({
|
||||||
|
method: "POST",
|
||||||
|
path: `/_matrix/client/unstable/org.matrix.msc3575/sync?timeout=${options.timeout}&pos=${options.pos}`,
|
||||||
|
body: options,
|
||||||
|
extra: { signal },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async fetchEvent(roomId, eventId) {
|
||||||
|
return this.fetch({
|
||||||
|
method: "GET",
|
||||||
|
path: `/_matrix/client/v3/rooms/${e(roomId)}/event/${e(eventId)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async fetchContext(roomId, eventId, limit = 50) {
|
||||||
|
return this.fetch({
|
||||||
|
method: "GET",
|
||||||
|
path: `/_matrix/client/v3/rooms/${e(roomId)}/context/${e(eventId)}?limit=${limit}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async fetchMessages(opts) {
|
||||||
|
let path = `/_matrix/client/v3/rooms/${e(opts.roomId)}/messages?limit=${opts.limit || 50}&from=${e(opts.from)}&dir=${e(opts.dir)}`;
|
||||||
|
if (opts.to)
|
||||||
|
path += `&to=${e(opts.to)}`;
|
||||||
|
return this.fetch({ method: "GET", path });
|
||||||
|
}
|
||||||
|
fetchRelations(roomId, eventId, opts) {
|
||||||
|
if (opts.eventType && !opts.relType)
|
||||||
|
throw new Error("you can't set eventType without relType");
|
||||||
|
let path = `/_matrix/client/v3/rooms/${e(roomId)}/relations/${e(eventId)}`;
|
||||||
|
if (opts.relType)
|
||||||
|
path += `/${e(opts.relType)}`;
|
||||||
|
if (opts.eventType)
|
||||||
|
path += `/${e(opts.eventType)}`;
|
||||||
|
path += `?limit=${opts.limit || 50}&dir=${opts.dir}`;
|
||||||
|
if (opts.from)
|
||||||
|
path += `&from=${opts.from}`;
|
||||||
|
if (opts.to)
|
||||||
|
path += `&from=${opts.to}`;
|
||||||
|
return this.fetch({ method: "GET", path });
|
||||||
|
}
|
||||||
|
async fetchThreads(opts) {
|
||||||
|
return this.fetch({
|
||||||
|
method: "POST",
|
||||||
|
path: `/_matrix/client/v1/threads?limit=${opts.limit}${opts.from ? `&from=${e(opts.from)}` : ""}`,
|
||||||
|
body: {
|
||||||
|
watching: opts.watching,
|
||||||
|
room_ids: opts.roomIds,
|
||||||
|
include: opts.include,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async fetchInbox(opts) {
|
||||||
|
return this.fetch({
|
||||||
|
method: "POST",
|
||||||
|
path: `/_matrix/client/v1/inbox?limit=${opts.limit}${opts.from ? `&from=${e(opts.from)}` : ""}`,
|
||||||
|
body: {
|
||||||
|
filter: opts.filter,
|
||||||
|
room_ids: opts.roomIds,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async sendEvent(roomId, type, txnId, content) {
|
||||||
|
return this.fetch({
|
||||||
|
method: "PUT",
|
||||||
|
path: `/_matrix/client/v3/rooms/${e(roomId)}/send/${e(type)}/${e(txnId)}`,
|
||||||
|
body: content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async sendState(roomId, type, stateKey, content) {
|
||||||
|
return this.fetch({
|
||||||
|
method: "PUT",
|
||||||
|
path: `/_matrix/client/v3/rooms/${e(roomId)}/state/${e(type)}/${e(stateKey)}`,
|
||||||
|
body: content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
30
dist/src/room.d.ts
vendored
Normal file
30
dist/src/room.d.ts
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import TypedEmitter from "typed-emitter";
|
||||||
|
import { ApiEphemeralEvent, SyncResponseRoom, Unreads } from "./api.js";
|
||||||
|
import { Client } from "./client.js";
|
||||||
|
import { Event, StateEvent } from "./event.js";
|
||||||
|
import { TimelineSet } from "./timeline.js";
|
||||||
|
type RoomEvents = {
|
||||||
|
timeline: (event: Event) => void;
|
||||||
|
ephemeral: (event: ApiEphemeralEvent) => void;
|
||||||
|
state: (event: StateEvent) => void;
|
||||||
|
notifications: (notifs: Unreads) => void;
|
||||||
|
summary: (x: {
|
||||||
|
invited_count: number;
|
||||||
|
joined_count: number;
|
||||||
|
}) => void;
|
||||||
|
accountData: (type: string, content: string) => void;
|
||||||
|
};
|
||||||
|
declare const Room_base: new () => TypedEmitter<RoomEvents>;
|
||||||
|
export declare class Room extends Room_base {
|
||||||
|
client: Client;
|
||||||
|
id: string;
|
||||||
|
private state;
|
||||||
|
timelines: TimelineSet;
|
||||||
|
constructor(client: Client, id: string, data: SyncResponseRoom);
|
||||||
|
_merge(data: SyncResponseRoom): void;
|
||||||
|
getState(type: string, stateKey?: string): StateEvent | null;
|
||||||
|
getAllState(type: string): Array<StateEvent>;
|
||||||
|
sendState(type: string, stateKey: string, content: any): Promise<void>;
|
||||||
|
sendEvent(type: string, content: any): Promise<void>;
|
||||||
|
}
|
||||||
|
export {};
|
69
dist/src/room.js
vendored
Normal file
69
dist/src/room.js
vendored
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import EventEmitter from "events";
|
||||||
|
import { Event, StateEvent } from "./event.js";
|
||||||
|
import { TimelineSet } from "./timeline.js";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
export class Room extends EventEmitter {
|
||||||
|
constructor(client, id, data) {
|
||||||
|
super();
|
||||||
|
Object.defineProperty(this, "client", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: client
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "id", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: id
|
||||||
|
});
|
||||||
|
// The (possibly incomplete) state of this room
|
||||||
|
Object.defineProperty(this, "state", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: new Map()
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "timelines", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
this.timelines = new TimelineSet(this);
|
||||||
|
this.timelines.live.prevBatch = data.prev_batch || null;
|
||||||
|
this._merge(data);
|
||||||
|
}
|
||||||
|
// should be private to only the library like pub(crate), but there's no way currently
|
||||||
|
_merge(data) {
|
||||||
|
for (const raw of data.required_state || []) {
|
||||||
|
const event = new StateEvent(this, raw);
|
||||||
|
const map = this.state.get(event.type);
|
||||||
|
if (map) {
|
||||||
|
map.set(event.stateKey, event);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.state.set(event.type, new Map([[event.stateKey, event]]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.timeline?.length) {
|
||||||
|
const events = data.timeline.map(raw => new Event(this, raw));
|
||||||
|
this.timelines._appendEvents(events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getState(type, stateKey = "") {
|
||||||
|
return this.state.get(type)?.get(stateKey) || null;
|
||||||
|
}
|
||||||
|
getAllState(type) {
|
||||||
|
return [...this.state.get(type)?.values() ?? []];
|
||||||
|
}
|
||||||
|
// TODO: return event
|
||||||
|
async sendState(type, stateKey, content) {
|
||||||
|
// const { event_id } = await this.client.net.sendState(this.id, type, stateKey, content);
|
||||||
|
await this.client.net.sendState(this.id, type, stateKey, content);
|
||||||
|
}
|
||||||
|
// TODO: return event
|
||||||
|
async sendEvent(type, content) {
|
||||||
|
await this.client.net.sendEvent(this.id, type, nanoid(), content);
|
||||||
|
}
|
||||||
|
}
|
2
dist/src/setup.d.ts
vendored
Normal file
2
dist/src/setup.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
declare class Setup {
|
||||||
|
}
|
4
dist/src/setup.js
vendored
Normal file
4
dist/src/setup.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
"use strict";
|
||||||
|
// Used for initiating a client, handling well-known and authentication.
|
||||||
|
class Setup {
|
||||||
|
}
|
19
dist/src/sync.d.ts
vendored
Normal file
19
dist/src/sync.d.ts
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import type { Client } from "./index.js";
|
||||||
|
import { ListSubscription, RoomSubscription } from "./api.js";
|
||||||
|
export declare class Connection {
|
||||||
|
private client;
|
||||||
|
private connId;
|
||||||
|
private controller;
|
||||||
|
private pos;
|
||||||
|
private delta;
|
||||||
|
private query;
|
||||||
|
constructor(client: Client);
|
||||||
|
sync(timeout?: number): Promise<import("./api.js").SyncResponse | undefined>;
|
||||||
|
private refresh;
|
||||||
|
private stripSticky;
|
||||||
|
listSubscribe(name: string, subscription: ListSubscription): void;
|
||||||
|
listUnsubscribe(name: string): void;
|
||||||
|
roomSubscribe(roomId: string, subscription: RoomSubscription): void;
|
||||||
|
roomUnsubscribe(roomId: string): void;
|
||||||
|
abort(reason?: string): void;
|
||||||
|
}
|
139
dist/src/sync.js
vendored
Normal file
139
dist/src/sync.js
vendored
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
// A sliding sync connection. One of these is active per client.
|
||||||
|
// It is part of `Client`, but split out for maintainability.
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { Room } from "./room.js";
|
||||||
|
export class Connection {
|
||||||
|
constructor(client) {
|
||||||
|
Object.defineProperty(this, "client", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: client
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "connId", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: nanoid()
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "controller", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: new AbortController()
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "pos", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: "0"
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "delta", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "query", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// run sync once
|
||||||
|
async sync(timeout = 30000) {
|
||||||
|
const json = await this.client.net.sync({
|
||||||
|
conn_id: this.connId,
|
||||||
|
pos: this.pos,
|
||||||
|
delta_token: this.delta || undefined,
|
||||||
|
timeout,
|
||||||
|
...this.query,
|
||||||
|
}, this.controller.signal).catch((reason) => {
|
||||||
|
if (reason === "update query")
|
||||||
|
return null;
|
||||||
|
throw reason;
|
||||||
|
});
|
||||||
|
if (!json)
|
||||||
|
return;
|
||||||
|
console.log(json);
|
||||||
|
this.stripSticky();
|
||||||
|
this.pos = json.pos;
|
||||||
|
this.delta = json.delta_token;
|
||||||
|
const { rooms, lists } = this.client;
|
||||||
|
for (const roomId in json.rooms) {
|
||||||
|
const data = json.rooms[roomId];
|
||||||
|
if (rooms.has(roomId) && !data.initial) {
|
||||||
|
rooms.get(roomId)._merge(data);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const room = new Room(this.client, roomId, data);
|
||||||
|
rooms.set(roomId, room);
|
||||||
|
this.client.emit("roomInit", room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const listId in json.lists) {
|
||||||
|
const list = lists.get(listId);
|
||||||
|
if (!list)
|
||||||
|
continue;
|
||||||
|
list.count = json.lists[listId].count;
|
||||||
|
for (const op of json.lists[listId].ops) {
|
||||||
|
switch (op.op) {
|
||||||
|
case "SYNC":
|
||||||
|
list.rooms.splice(op.range[0], op.range[1] - op.range[0] + 1, ...op.room_ids.map(id => rooms.get(id)));
|
||||||
|
break;
|
||||||
|
// case "INVALIDATE":
|
||||||
|
// list.rooms.splice(op.range[0], op.range[1] - op.range[0] + 1);
|
||||||
|
// break;
|
||||||
|
default:
|
||||||
|
// the entire op system should probably be reworked
|
||||||
|
// if there's no sorting, i only need `sync` and `invalidate`?
|
||||||
|
throw new Error("unimplemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
refresh() {
|
||||||
|
this.controller.abort("update query");
|
||||||
|
this.controller = new AbortController();
|
||||||
|
}
|
||||||
|
stripSticky() {
|
||||||
|
const { query } = this;
|
||||||
|
const { lists } = query;
|
||||||
|
if (lists) {
|
||||||
|
for (const list in lists) {
|
||||||
|
lists[list] = { ranges: lists[list].ranges };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query.queries = {};
|
||||||
|
query.rooms = {};
|
||||||
|
}
|
||||||
|
listSubscribe(name, subscription) {
|
||||||
|
if (!this.query.lists)
|
||||||
|
this.query.lists = {};
|
||||||
|
this.query.lists[name] = subscription;
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
listUnsubscribe(name) {
|
||||||
|
if (!this.query.lists)
|
||||||
|
return;
|
||||||
|
delete this.query.lists[name];
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
roomSubscribe(roomId, subscription) {
|
||||||
|
if (!this.query.rooms)
|
||||||
|
this.query.rooms = {};
|
||||||
|
this.query.rooms[roomId] = subscription;
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
roomUnsubscribe(roomId) {
|
||||||
|
if (!this.query.rooms)
|
||||||
|
this.query.rooms = {};
|
||||||
|
this.query.rooms[roomId] = null;
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
abort(reason) {
|
||||||
|
this.controller.abort(reason);
|
||||||
|
}
|
||||||
|
}
|
15
dist/src/thread.d.ts
vendored
Normal file
15
dist/src/thread.d.ts
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import TypedEmitter from "typed-emitter";
|
||||||
|
import { Unreads } from "./api.js";
|
||||||
|
import { Room } from "./room.js";
|
||||||
|
import { Event } from "./event.js";
|
||||||
|
type ThreadEvents = {
|
||||||
|
timeline: (event: Event) => void;
|
||||||
|
notifications: (notifs: Unreads) => void;
|
||||||
|
};
|
||||||
|
declare const Thread_base: new () => TypedEmitter<ThreadEvents>;
|
||||||
|
export declare class Thread extends Thread_base {
|
||||||
|
baseEvent: Event;
|
||||||
|
room: Room;
|
||||||
|
constructor(baseEvent: Event);
|
||||||
|
}
|
||||||
|
export {};
|
18
dist/src/thread.js
vendored
Normal file
18
dist/src/thread.js
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import EventEmitter from "events";
|
||||||
|
export class Thread extends EventEmitter {
|
||||||
|
constructor(baseEvent) {
|
||||||
|
super();
|
||||||
|
Object.defineProperty(this, "baseEvent", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: baseEvent
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "room", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: this.baseEvent.room
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
33
dist/src/timeline.d.ts
vendored
Normal file
33
dist/src/timeline.d.ts
vendored
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { EventId } from "./api.js";
|
||||||
|
import { Room } from "./room.js";
|
||||||
|
import { Event } from "./event.js";
|
||||||
|
import { Thread } from "./thread.js";
|
||||||
|
interface Timeline {
|
||||||
|
isLive: boolean;
|
||||||
|
events: Array<Event>;
|
||||||
|
}
|
||||||
|
export declare class RoomTimeline implements Timeline {
|
||||||
|
room: Room;
|
||||||
|
events: Array<Event>;
|
||||||
|
isLive: boolean;
|
||||||
|
prevBatch: string | null;
|
||||||
|
nextBatch: string | null;
|
||||||
|
constructor(room: Room);
|
||||||
|
}
|
||||||
|
export declare class ThreadTimeline implements Timeline {
|
||||||
|
thread: Thread;
|
||||||
|
events: never[];
|
||||||
|
isLive: boolean;
|
||||||
|
prevBatch: string | null;
|
||||||
|
nextBatch: string | null;
|
||||||
|
constructor(thread: Thread);
|
||||||
|
}
|
||||||
|
export declare class TimelineSet {
|
||||||
|
room: Room;
|
||||||
|
live: RoomTimeline;
|
||||||
|
private timelines;
|
||||||
|
constructor(room: Room);
|
||||||
|
forEvent(eventId: EventId): Promise<RoomTimeline>;
|
||||||
|
_appendEvents(events: Array<Event>): void;
|
||||||
|
}
|
||||||
|
export {};
|
129
dist/src/timeline.js
vendored
Normal file
129
dist/src/timeline.js
vendored
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
// Timelines are ordered sequences of events.
|
||||||
|
import { Event } from "./event.js";
|
||||||
|
export class RoomTimeline {
|
||||||
|
constructor(room) {
|
||||||
|
Object.defineProperty(this, "room", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: room
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "events", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: []
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "isLive", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: false
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "prevBatch", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: null
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "nextBatch", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class ThreadTimeline {
|
||||||
|
constructor(thread) {
|
||||||
|
Object.defineProperty(this, "thread", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: thread
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "events", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: []
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "isLive", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: false
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "prevBatch", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: null
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "nextBatch", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class TimelineSet {
|
||||||
|
// private eventIdToTimeline: Map<EventId, Timeline> = new Map();
|
||||||
|
constructor(room) {
|
||||||
|
Object.defineProperty(this, "room", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: room
|
||||||
|
});
|
||||||
|
// Other timelines *may* be live, but this one is guaranteed to be live
|
||||||
|
Object.defineProperty(this, "live", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: void 0
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "timelines", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: new Set()
|
||||||
|
});
|
||||||
|
this.live = new RoomTimeline(room);
|
||||||
|
this.live.isLive = true;
|
||||||
|
this.timelines.add(this.live);
|
||||||
|
}
|
||||||
|
// Get a timeline for a thread. Becomes a live timeline if `atEnd = true`.
|
||||||
|
// public async forThread(eventId: EventId, atEnd: boolean): Promise<ThreadTimeline> {
|
||||||
|
// throw "todo";
|
||||||
|
// // const thread = new Thread();
|
||||||
|
// // const tl = new ThreadTimeline(thread);
|
||||||
|
// // return tl;
|
||||||
|
// }
|
||||||
|
// Get a timeline for an event (context).
|
||||||
|
async forEvent(eventId) {
|
||||||
|
const context = await this.room.client.net.fetchContext(this.room.id, eventId);
|
||||||
|
const tl = new RoomTimeline(this.room);
|
||||||
|
tl.events = context.events_before
|
||||||
|
.reverse()
|
||||||
|
.concat([context.event])
|
||||||
|
.concat(context.events_after)
|
||||||
|
.map(raw => new Event(this.room, raw));
|
||||||
|
tl.prevBatch = context.start;
|
||||||
|
tl.nextBatch = context.end;
|
||||||
|
this.timelines.add(tl);
|
||||||
|
return tl;
|
||||||
|
}
|
||||||
|
// Paginate a timeline for more events
|
||||||
|
// public paginate(timeline: Timeline, dir: "f" | "b", limit: number = 50) {
|
||||||
|
// throw "todo";
|
||||||
|
// }
|
||||||
|
_appendEvents(events) {
|
||||||
|
// FIXME: merge timelines together
|
||||||
|
for (const timeline of this.timelines) {
|
||||||
|
if (timeline.isLive)
|
||||||
|
timeline.events.push(...events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
0
dist/src/utils.d.ts
vendored
Normal file
0
dist/src/utils.d.ts
vendored
Normal file
2
dist/src/utils.js
vendored
Normal file
2
dist/src/utils.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
"use strict";
|
||||||
|
// Various useful utilities
|
2
dist/tsconfig.tsbuildinfo
vendored
2
dist/tsconfig.tsbuildinfo
vendored
File diff suppressed because one or more lines are too long
22
docs/name.md
Normal file
22
docs/name.md
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
```ts
|
||||||
|
function getName(room: Room): string {
|
||||||
|
const nameEvent = room.getState("m.room.name", "")?.content;
|
||||||
|
if (nameEvent) return nameEvent;
|
||||||
|
|
||||||
|
const purposes = room.getState("m.room.purpose", "")?.content?.purposes;
|
||||||
|
if (Array.isArray(purposes) && purposes.includes("m.direct")) {
|
||||||
|
const members = room.getAllState("m.room.member")
|
||||||
|
.filter(i => i.content.membership === "join")
|
||||||
|
.map(i => i.content.displayname || i.stateKey);
|
||||||
|
switch (members.length) {
|
||||||
|
case 1: return members[0];
|
||||||
|
case 2: return `${members[0]} and ${members[1]}`;
|
||||||
|
case 3: return `${members[0]}, ${members[1]}, and ${members[2]}`;
|
||||||
|
case 4: return `${members[0]}, ${members[1]}, ${members[2]}, and ${members[3]}`;
|
||||||
|
default: return `${members[0]}, ${members[1]}, ${members[2]}, and ${members.length - 3} others`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Unnamed room";
|
||||||
|
}
|
||||||
|
```
|
34
docs/stream.md
Normal file
34
docs/stream.md
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
During media upload, it would be nice to know the progress of an
|
||||||
|
upload. This is possible with a `TransformStream`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function progress() {
|
||||||
|
let bytes = 0;
|
||||||
|
return new TransformStream({
|
||||||
|
transform(chunk, control) {
|
||||||
|
if (chunk === null) {
|
||||||
|
control.terminate();
|
||||||
|
} else if (ArrayBuffer.isView(chunk)) {
|
||||||
|
bytes += chunk.byteLength;
|
||||||
|
console.log(`sent chunk (size = ${chunk.byteLength}, total = ${bytes})`);
|
||||||
|
control.enqueue(chunk);
|
||||||
|
} else {
|
||||||
|
throw new Error("invalid bytes");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use it in ie. Deno as follows:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const file = await Deno.open("file.ext");
|
||||||
|
const { readable, writable } = progress();
|
||||||
|
file.readable.pipeTo(writable);
|
||||||
|
const req = await fetch("https://httpbin.org/anything", {
|
||||||
|
method: "POST",
|
||||||
|
body: readable,
|
||||||
|
});
|
||||||
|
console.log(req);
|
||||||
|
```
|
|
@ -10,6 +10,12 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/events": "^3.0.3",
|
||||||
"typescript": "^5.3.2"
|
"typescript": "^5.3.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"nanoid": "^5.0.4",
|
||||||
|
"typed-emitter": "^2.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,62 @@ settings:
|
||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
events:
|
||||||
|
specifier: ^3.3.0
|
||||||
|
version: 3.3.0
|
||||||
|
nanoid:
|
||||||
|
specifier: ^5.0.4
|
||||||
|
version: 5.0.4
|
||||||
|
typed-emitter:
|
||||||
|
specifier: ^2.1.0
|
||||||
|
version: 2.1.0
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/events':
|
||||||
|
specifier: ^3.0.3
|
||||||
|
version: 3.0.3
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.3.2
|
specifier: ^5.3.2
|
||||||
version: 5.3.2
|
version: 5.3.2
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
/@types/events@3.0.3:
|
||||||
|
resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/events@3.3.0:
|
||||||
|
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||||
|
engines: {node: '>=0.8.x'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/nanoid@5.0.4:
|
||||||
|
resolution: {integrity: sha512-vAjmBf13gsmhXSgBrtIclinISzFFy22WwCYoyilZlsrRXNIHSwgFQ1bEdjRwMT3aoadeIF6HMuDRlOxzfXV8ig==}
|
||||||
|
engines: {node: ^18 || >=20}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/rxjs@7.8.1:
|
||||||
|
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
|
||||||
|
requiresBuild: true
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.6.2
|
||||||
|
dev: false
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/tslib@2.6.2:
|
||||||
|
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||||
|
requiresBuild: true
|
||||||
|
dev: false
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/typed-emitter@2.1.0:
|
||||||
|
resolution: {integrity: sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==}
|
||||||
|
optionalDependencies:
|
||||||
|
rxjs: 7.8.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/typescript@5.3.2:
|
/typescript@5.3.2:
|
||||||
resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==}
|
resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
|
|
200
src/api.ts
Normal file
200
src/api.ts
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
// all the types for the api
|
||||||
|
|
||||||
|
export type RoomId = string;
|
||||||
|
export type UserId = string;
|
||||||
|
export type EventId = string;
|
||||||
|
|
||||||
|
export interface ApiEvent {
|
||||||
|
event_id: EventId,
|
||||||
|
sender: UserId,
|
||||||
|
type: string,
|
||||||
|
content: any,
|
||||||
|
state_key?: string,
|
||||||
|
unsigned?: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiStateEvent = ApiEvent & {
|
||||||
|
state_key: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiDeviceEvent = {
|
||||||
|
sender: UserId,
|
||||||
|
type: string,
|
||||||
|
content: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiEphemeralEvent = {
|
||||||
|
sender: UserId,
|
||||||
|
type: string,
|
||||||
|
content: any,
|
||||||
|
key?: string,
|
||||||
|
persist_until?: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RoomSubscription = {
|
||||||
|
query: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListSubscription = {
|
||||||
|
ranges?: Array<[number, number]>,
|
||||||
|
query?: string,
|
||||||
|
filters?: {
|
||||||
|
spaces?: Array<RoomId | null>,
|
||||||
|
types?: Array<string | null>,
|
||||||
|
purposes?: Array<string | null>,
|
||||||
|
is_invite_knock?: boolean,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// how much do i split it? do i make ephemeral events their own extension?
|
||||||
|
export interface SyncRequest {
|
||||||
|
pos?: string,
|
||||||
|
timeout?: number,
|
||||||
|
txn_id?: string,
|
||||||
|
conn_id?: string,
|
||||||
|
delta_token?: string,
|
||||||
|
queries?: Record<string, null | {
|
||||||
|
required_state?: Array<[string, string]>,
|
||||||
|
timeline?: {
|
||||||
|
limit?: number,
|
||||||
|
types?: Array<string>,
|
||||||
|
not_types?: Array<string>,
|
||||||
|
|
||||||
|
// theoretically, the best option would be to hide m.reaction events
|
||||||
|
// in the historical timeline while showing them in the live timeline
|
||||||
|
// i'd need to implement proper filtering for that though
|
||||||
|
// live_filter?: string,
|
||||||
|
// historical_filter?: string,
|
||||||
|
},
|
||||||
|
ephemeral?: {
|
||||||
|
limit?: number,
|
||||||
|
types?: Array<string>,
|
||||||
|
include_old?: boolean, // include temporarily persisted events replaced with the same key
|
||||||
|
},
|
||||||
|
}>
|
||||||
|
lists?: Record<string, null | ListSubscription>,
|
||||||
|
rooms?: Record<RoomId, null | RoomSubscription>,
|
||||||
|
extensions?: {
|
||||||
|
"m.to_device"?: {
|
||||||
|
enabled: boolean,
|
||||||
|
limit?: number,
|
||||||
|
},
|
||||||
|
"m.e2ee"?: {
|
||||||
|
enabled: boolean,
|
||||||
|
},
|
||||||
|
"m.account_data"?: {
|
||||||
|
enabled: boolean,
|
||||||
|
lists: Array<string>,
|
||||||
|
rooms: Array<RoomId>,
|
||||||
|
},
|
||||||
|
"m.presence"?: {
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncResponseRoom {
|
||||||
|
initial?: boolean,
|
||||||
|
required_state?: Array<ApiStateEvent>,
|
||||||
|
timeline?: Array<ApiEvent>,
|
||||||
|
ephemeral?: Array<ApiEphemeralEvent>,
|
||||||
|
prev_batch?: string,
|
||||||
|
joined_count?: number,
|
||||||
|
invited_count?: number,
|
||||||
|
unreads?: Unreads,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncResponse {
|
||||||
|
pos: string,
|
||||||
|
txn_id?: string,
|
||||||
|
delta_token?: string,
|
||||||
|
lists?: Record<string, {
|
||||||
|
count: number,
|
||||||
|
ops: Array<
|
||||||
|
// i may rework this api to be less awful
|
||||||
|
{ op: "SYNC", range: [number, number], room_ids: Array<RoomId> } |
|
||||||
|
{ op: "INSERT", index: number, room_id: RoomId } |
|
||||||
|
{ op: "DELETE", index: number, room_ids: Array<RoomId> } |
|
||||||
|
{ op: "INVALIDATE", range: [number, number] }
|
||||||
|
>
|
||||||
|
}>,
|
||||||
|
rooms?: Record<RoomId, SyncResponseRoom>,
|
||||||
|
extensions?: {
|
||||||
|
"m.to_device"?: {
|
||||||
|
enabled: boolean,
|
||||||
|
events: Array<ApiDeviceEvent>,
|
||||||
|
},
|
||||||
|
"m.account_data"?: {
|
||||||
|
enabled: boolean,
|
||||||
|
lists: Array<string>,
|
||||||
|
rooms: Array<RoomId>,
|
||||||
|
},
|
||||||
|
"m.e2ee"?: {
|
||||||
|
"device_one_time_keys_count": {
|
||||||
|
"signed_curve25519": number,
|
||||||
|
},
|
||||||
|
"device_lists": {
|
||||||
|
"changed": Array<UserId>,
|
||||||
|
"left": Array<UserId>,
|
||||||
|
},
|
||||||
|
"device_unused_fallback_key_types": [
|
||||||
|
"signed_curve25519"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"m.presence"?: {
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Unreads {
|
||||||
|
last_ack?: EventId,
|
||||||
|
mention_user?: number,
|
||||||
|
mention_bulk?: number, // @room (entire room) or @thread (in a thread)
|
||||||
|
notify?: number,
|
||||||
|
messages?: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContextResponse {
|
||||||
|
start: string,
|
||||||
|
end: string,
|
||||||
|
event: ApiEvent,
|
||||||
|
events_before: Array<ApiEvent>, // in reverse chronological order!
|
||||||
|
events_after: Array<ApiEvent>,
|
||||||
|
state: Array<ApiStateEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessagesResponse {
|
||||||
|
chunk: Array<ApiEvent>, // in reverse chronological order, if dir=b!
|
||||||
|
start: string,
|
||||||
|
end: string,
|
||||||
|
state: Array<ApiStateEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelationsResponse {
|
||||||
|
chunk: Array<ApiEvent>,
|
||||||
|
next_batch: string,
|
||||||
|
prev_batch: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendResponse {
|
||||||
|
event_id: EventId,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InboxFilter = "default" | "mentions_user" | "mentions_bulk" | "threads_participating" | "threads_interesting" | "include_read";
|
||||||
|
|
||||||
|
export interface InboxResponse {
|
||||||
|
next_batch?: string,
|
||||||
|
threads?: Array<ApiEvent>,
|
||||||
|
chunk?: Array<{
|
||||||
|
event: ApiEvent,
|
||||||
|
read: boolean,
|
||||||
|
}>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IncludeThreads = "read" | "ignoring";
|
||||||
|
|
||||||
|
export interface ThreadsResponse {
|
||||||
|
chunk?: Array<Event>,
|
||||||
|
next_batch?: string,
|
||||||
|
}
|
111
src/client.ts
Normal file
111
src/client.ts
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
// The "main" class that all interaction can be done through.
|
||||||
|
|
||||||
|
import { ApiDeviceEvent, ListSubscription, RoomSubscription } from "./api.js";
|
||||||
|
import { Network } from "./net.js";
|
||||||
|
import { Room } from "./room.js";
|
||||||
|
import { Connection } from "./sync.js";
|
||||||
|
import EventEmitter from "events";
|
||||||
|
import TypedEmitter from "typed-emitter";
|
||||||
|
|
||||||
|
interface ClientConfig {
|
||||||
|
baseUrl: string,
|
||||||
|
token: string,
|
||||||
|
userId: string,
|
||||||
|
deviceId: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientState = { state: "stop" } // The client is stopped and inactive
|
||||||
|
| { state: "sync" } // The client is active and syncing
|
||||||
|
| { state: "catchup" } // The client is catching up after a `retry`
|
||||||
|
| { state: "error", reason: any } // The client failed and will not retry
|
||||||
|
| { state: "retry", backoff: number } // The client failed and is retrying
|
||||||
|
|
||||||
|
type ClientEvents = {
|
||||||
|
// The client's state changed.
|
||||||
|
state: (state: ClientState) => void,
|
||||||
|
|
||||||
|
// A list is created or updated.
|
||||||
|
list: (name: string, list: RoomList) => void,
|
||||||
|
|
||||||
|
// A room is initialized for the first time, and is now available to
|
||||||
|
// interact with. This may be executed multiple times, if the homeserver
|
||||||
|
// resends `initial: true`
|
||||||
|
roomInit: (room: Room) => void,
|
||||||
|
|
||||||
|
// A room is invalidated and has stale data. It is still accessible,
|
||||||
|
// but is not in any lists and only exists as cache.
|
||||||
|
roomDeinit: (room: Room) => void,
|
||||||
|
|
||||||
|
// Global account data is updated.
|
||||||
|
accountData: (type: string, content: string) => void,
|
||||||
|
|
||||||
|
// A `to_device` message was received.
|
||||||
|
toDevice: (event: ApiDeviceEvent) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
type RoomList = {
|
||||||
|
// The total number of rooms in this list
|
||||||
|
count: number,
|
||||||
|
|
||||||
|
// The currently visible room list
|
||||||
|
rooms: Array<Room>,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Client extends (EventEmitter as unknown as new () => TypedEmitter<ClientEvents>) {
|
||||||
|
// these shouldn't be public, but ah typescript
|
||||||
|
state: ClientState = { state: "stop" };
|
||||||
|
net: Network;
|
||||||
|
conn: Connection;
|
||||||
|
|
||||||
|
public lists: Map<string, RoomList> = new Map();
|
||||||
|
public rooms: Map<string, Room> = new Map();
|
||||||
|
|
||||||
|
constructor(public config: ClientConfig) {
|
||||||
|
super();
|
||||||
|
this.net = new Network(this, {
|
||||||
|
verbose: true,
|
||||||
|
});
|
||||||
|
this.conn = new Connection(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start receiving events from /sync.
|
||||||
|
// WARN: if you lose the reference to Client, the poller will leak
|
||||||
|
start() {
|
||||||
|
this.state = { state: "sync" };
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
while (this.state.state === "sync") {
|
||||||
|
try {
|
||||||
|
await this.conn.sync();
|
||||||
|
} catch (err) {
|
||||||
|
this.state = { state: "error", reason: err };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop receiving events from /sync.
|
||||||
|
stop() {
|
||||||
|
this.conn.abort();
|
||||||
|
this.conn = new Connection(this);
|
||||||
|
this.state = { state: "stop" };
|
||||||
|
}
|
||||||
|
|
||||||
|
listSubscribe(name: string, subscription: ListSubscription) {
|
||||||
|
this.lists.set(name, { count: 0, rooms: [] });
|
||||||
|
this.conn.listSubscribe(name, subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
listUnsubscribe(name: string) {
|
||||||
|
this.lists.delete(name);
|
||||||
|
this.conn.listUnsubscribe(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
roomSubscribe(roomId: string, subscription: RoomSubscription) {
|
||||||
|
this.conn.roomSubscribe(roomId, subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
roomUnsubscribe(roomId: string) {
|
||||||
|
this.conn.roomUnsubscribe(roomId);
|
||||||
|
}
|
||||||
|
}
|
34
src/event.ts
Normal file
34
src/event.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { Client } from "./client.js";
|
||||||
|
import { ApiEvent, ApiStateEvent, EventId, UserId } from "./api.js";
|
||||||
|
import { Room } from "./room.js";
|
||||||
|
|
||||||
|
export class Event {
|
||||||
|
public client: Client = this.room.client;
|
||||||
|
public content: any;
|
||||||
|
public id: EventId;
|
||||||
|
public sender: UserId;
|
||||||
|
public stateKey?: string;
|
||||||
|
public type: string;
|
||||||
|
public unsigned: any;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public room: Room,
|
||||||
|
json: ApiEvent
|
||||||
|
) {
|
||||||
|
this.content = json.content;
|
||||||
|
this.id = json.event_id;
|
||||||
|
this.sender = json.sender;
|
||||||
|
this.stateKey = json.state_key;
|
||||||
|
this.type = json.type;
|
||||||
|
this.unsigned = json.unsigned;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StateEvent extends Event {
|
||||||
|
public stateKey: string;
|
||||||
|
|
||||||
|
constructor(room: Room, json: ApiStateEvent) {
|
||||||
|
super(room, json);
|
||||||
|
this.stateKey = json.state_key;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1 @@
|
||||||
export function hello(name: string) {
|
export { Client } from "./client.js";
|
||||||
console.log(`hello ${name}!`);
|
|
||||||
}
|
|
||||||
|
|
146
src/net.ts
Normal file
146
src/net.ts
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
// Handles *all* network/http requests.
|
||||||
|
import * as t from "./api.js";
|
||||||
|
import { Client } from "./client.js";
|
||||||
|
|
||||||
|
const e = (s: string) => encodeURIComponent(s);
|
||||||
|
|
||||||
|
interface NetworkConfig {
|
||||||
|
fetchImpl?: typeof fetch,
|
||||||
|
verbose?: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMethodColor(method: string): string {
|
||||||
|
// i love using the more obscure css colors
|
||||||
|
switch (method) {
|
||||||
|
case "GET": return "deepskyblue";
|
||||||
|
case "POST": return "springgreen";
|
||||||
|
case "PUT": return "lightsalmon";
|
||||||
|
case "PATCH": return "sandybrown";
|
||||||
|
case "DELETE": return "salmon";
|
||||||
|
default: return "orchid";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Network {
|
||||||
|
constructor(private client: Client, private config: NetworkConfig) {
|
||||||
|
if (!config.fetchImpl) config.fetchImpl = globalThis.fetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetch(options: {
|
||||||
|
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
|
||||||
|
path: string,
|
||||||
|
body?: BodyInit | object,
|
||||||
|
raw?: boolean,
|
||||||
|
extra?: RequestInit,
|
||||||
|
}) {
|
||||||
|
const url = `${this.client.config.baseUrl}${options.path}`;
|
||||||
|
const isJson = typeof options.body === "object"
|
||||||
|
&& !ArrayBuffer.isView(options.body)
|
||||||
|
&& !(options.body instanceof ReadableStream);
|
||||||
|
|
||||||
|
if (this.config.verbose) {
|
||||||
|
console.log("%c%s%c %s", getMethodColor(options.method), options.method, "color: initial", options.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = await this.config.fetchImpl!(url, {
|
||||||
|
method: options.method,
|
||||||
|
headers: {
|
||||||
|
"authorization": `Bearer ${this.client.config.token}`,
|
||||||
|
},
|
||||||
|
body: (isJson ? JSON.stringify(options.body) : options.body) as BodyInit | undefined,
|
||||||
|
...options.extra,
|
||||||
|
});
|
||||||
|
if (!req.ok) throw new Error(`Request failed: ${await req.text()}`);
|
||||||
|
if (options.raw) return req.body;
|
||||||
|
return req.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sync(options: t.SyncRequest, signal: AbortSignal): Promise<t.SyncResponse> {
|
||||||
|
return this.fetch({
|
||||||
|
method: "POST",
|
||||||
|
path: `/_matrix/client/unstable/org.matrix.msc3575/sync?timeout=${options.timeout}&pos=${options.pos}`,
|
||||||
|
body: options,
|
||||||
|
extra: { signal },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async fetchEvent(roomId: t.RoomId, eventId: t.EventId): Promise<t.ApiEvent> {
|
||||||
|
return this.fetch({
|
||||||
|
method: "GET",
|
||||||
|
path: `/_matrix/client/v3/rooms/${e(roomId)}/event/${e(eventId)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async fetchContext(roomId: t.RoomId, eventId: t.EventId, limit = 50): Promise<t.ContextResponse> {
|
||||||
|
return this.fetch({
|
||||||
|
method: "GET",
|
||||||
|
path: `/_matrix/client/v3/rooms/${e(roomId)}/context/${e(eventId)}?limit=${limit}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async fetchMessages(opts: { roomId: t.RoomId, dir: "b" | "f", limit?: number, from: string, to?: string }): Promise<t.MessagesResponse> {
|
||||||
|
let path = `/_matrix/client/v3/rooms/${e(opts.roomId)}/messages?limit=${opts.limit || 50}&from=${e(opts.from)}&dir=${e(opts.dir)}`;
|
||||||
|
if (opts.to) path += `&to=${e(opts.to)}`;
|
||||||
|
return this.fetch({ method: "GET", path });
|
||||||
|
}
|
||||||
|
|
||||||
|
public fetchRelations(
|
||||||
|
roomId: t.RoomId,
|
||||||
|
eventId: t.EventId,
|
||||||
|
opts: {
|
||||||
|
relType?: string,
|
||||||
|
eventType?: string,
|
||||||
|
limit?: string,
|
||||||
|
dir: "b" | "f",
|
||||||
|
from?: string,
|
||||||
|
to?: string,
|
||||||
|
}): Promise<t.RelationsResponse> {
|
||||||
|
if (opts.eventType && !opts.relType) throw new Error("you can't set eventType without relType");
|
||||||
|
let path = `/_matrix/client/v3/rooms/${e(roomId)}/relations/${e(eventId)}`;
|
||||||
|
if (opts.relType) path += `/${e(opts.relType)}`;
|
||||||
|
if (opts.eventType) path += `/${e(opts.eventType)}`;
|
||||||
|
path += `?limit=${opts.limit || 50}&dir=${opts.dir}`;
|
||||||
|
if (opts.from) path += `&from=${opts.from}`;
|
||||||
|
if (opts.to) path += `&from=${opts.to}`;
|
||||||
|
return this.fetch({ method: "GET", path });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async fetchThreads(opts: { from?: string, limit?: number, roomIds?: Array<t.RoomId>, watching: boolean, include: Array<t.IncludeThreads> }): Promise<t.ThreadsResponse> {
|
||||||
|
return this.fetch({
|
||||||
|
method: "POST",
|
||||||
|
path: `/_matrix/client/v1/threads?limit=${opts.limit}${opts.from ? `&from=${e(opts.from)}`: ""}`,
|
||||||
|
body: {
|
||||||
|
watching: opts.watching,
|
||||||
|
room_ids: opts.roomIds,
|
||||||
|
include: opts.include,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async fetchInbox(opts: { roomIds?: Array<t.RoomId>, from?: string, filter?: t.InboxFilter, limit?: number }): Promise<t.InboxResponse> {
|
||||||
|
return this.fetch({
|
||||||
|
method: "POST",
|
||||||
|
path: `/_matrix/client/v1/inbox?limit=${opts.limit}${opts.from ? `&from=${e(opts.from)}`: ""}`,
|
||||||
|
body: {
|
||||||
|
filter: opts.filter,
|
||||||
|
room_ids: opts.roomIds,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendEvent(roomId: t.RoomId, type: string, txnId: string, content: any): Promise<t.SendResponse> {
|
||||||
|
return this.fetch({
|
||||||
|
method: "PUT",
|
||||||
|
path: `/_matrix/client/v3/rooms/${e(roomId)}/send/${e(type)}/${e(txnId)}`,
|
||||||
|
body: content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendState(roomId: t.RoomId, type: string, stateKey: string, content: any): Promise<t.SendResponse> {
|
||||||
|
return this.fetch({
|
||||||
|
method: "PUT",
|
||||||
|
path: `/_matrix/client/v3/rooms/${e(roomId)}/state/${e(type)}/${e(stateKey)}`,
|
||||||
|
body: content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
81
src/room.ts
Normal file
81
src/room.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import EventEmitter from "events";
|
||||||
|
import TypedEmitter from "typed-emitter";
|
||||||
|
import { ApiEphemeralEvent, SyncResponseRoom, Unreads } from "./api.js";
|
||||||
|
import { Client } from "./client.js";
|
||||||
|
import { Event, StateEvent } from "./event.js";
|
||||||
|
import { TimelineSet } from "./timeline.js";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
type RoomEvents = {
|
||||||
|
// an event is appended to this room's live timeline
|
||||||
|
timeline: (event: Event) => void,
|
||||||
|
|
||||||
|
// an ephemeral event was received
|
||||||
|
ephemeral: (event: ApiEphemeralEvent) => void,
|
||||||
|
|
||||||
|
// this room's state updated
|
||||||
|
state: (event: StateEvent) => void,
|
||||||
|
|
||||||
|
// notifications for this room were updated
|
||||||
|
notifications: (notifs: Unreads) => void,
|
||||||
|
|
||||||
|
// this room's summary was updated
|
||||||
|
summary: (x: { invited_count: number, joined_count: number }) => void,
|
||||||
|
|
||||||
|
// accountdata is updated
|
||||||
|
accountData: (type: string, content: string) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Room extends (EventEmitter as unknown as new () => TypedEmitter<RoomEvents>) {
|
||||||
|
// The (possibly incomplete) state of this room
|
||||||
|
private state: Map<string, Map<string, StateEvent>> = new Map();
|
||||||
|
|
||||||
|
public timelines: TimelineSet;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public client: Client,
|
||||||
|
public id: string,
|
||||||
|
data: SyncResponseRoom,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.timelines = new TimelineSet(this);
|
||||||
|
this.timelines.live.prevBatch = data.prev_batch || null;
|
||||||
|
this._merge(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// should be private to only the library like pub(crate), but there's no way currently
|
||||||
|
_merge(data: SyncResponseRoom) {
|
||||||
|
for (const raw of data.required_state || []) {
|
||||||
|
const event = new StateEvent(this, raw);
|
||||||
|
const map = this.state.get(event.type);
|
||||||
|
if (map) {
|
||||||
|
map.set(event.stateKey, event);
|
||||||
|
} else {
|
||||||
|
this.state.set(event.type, new Map([[event.stateKey, event]]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.timeline?.length) {
|
||||||
|
const events = data.timeline.map(raw => new Event(this, raw));
|
||||||
|
this.timelines._appendEvents(events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getState(type: string, stateKey: string = ""): StateEvent | null {
|
||||||
|
return this.state.get(type)?.get(stateKey) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllState(type: string): Array<StateEvent> {
|
||||||
|
return [...this.state.get(type)?.values() ?? []];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: return event
|
||||||
|
async sendState(type: string, stateKey: string, content: any) {
|
||||||
|
// const { event_id } = await this.client.net.sendState(this.id, type, stateKey, content);
|
||||||
|
await this.client.net.sendState(this.id, type, stateKey, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: return event
|
||||||
|
async sendEvent(type: string, content: any) {
|
||||||
|
await this.client.net.sendEvent(this.id, type, nanoid(), content);
|
||||||
|
}
|
||||||
|
}
|
4
src/setup.ts
Normal file
4
src/setup.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
// Used for initiating a client, handling well-known and authentication.
|
||||||
|
|
||||||
|
class Setup {
|
||||||
|
}
|
121
src/sync.ts
Normal file
121
src/sync.ts
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
// A sliding sync connection. One of these is active per client.
|
||||||
|
// It is part of `Client`, but split out for maintainability.
|
||||||
|
|
||||||
|
import type { Client } from "./index.js";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { ListSubscription, RoomSubscription, SyncRequest } from "./api.js";
|
||||||
|
import { Room } from "./room.js";
|
||||||
|
|
||||||
|
export class Connection {
|
||||||
|
private connId = nanoid();
|
||||||
|
private controller = new AbortController();
|
||||||
|
private pos: string = "0";
|
||||||
|
private delta: string | undefined;
|
||||||
|
private query: SyncRequest = {};
|
||||||
|
|
||||||
|
constructor(private client: Client) {}
|
||||||
|
|
||||||
|
// run sync once
|
||||||
|
async sync(timeout: number = 30000) {
|
||||||
|
const json = await this.client.net.sync({
|
||||||
|
conn_id: this.connId,
|
||||||
|
pos: this.pos,
|
||||||
|
delta_token: this.delta || undefined,
|
||||||
|
timeout,
|
||||||
|
...this.query,
|
||||||
|
}, this.controller.signal).catch((reason) => {
|
||||||
|
if (reason === "update query") return null;
|
||||||
|
throw reason;
|
||||||
|
});
|
||||||
|
if (!json) return;
|
||||||
|
console.log(json)
|
||||||
|
|
||||||
|
this.stripSticky();
|
||||||
|
this.pos = json.pos;
|
||||||
|
this.delta = json.delta_token;
|
||||||
|
|
||||||
|
const { rooms, lists } = this.client;
|
||||||
|
|
||||||
|
for (const roomId in json.rooms) {
|
||||||
|
const data = json.rooms[roomId];
|
||||||
|
if (rooms.has(roomId) && !data.initial) {
|
||||||
|
rooms.get(roomId)!._merge(data);
|
||||||
|
} else {
|
||||||
|
const room = new Room(this.client, roomId, data);
|
||||||
|
rooms.set(roomId, room);
|
||||||
|
this.client.emit("roomInit", room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const listId in json.lists) {
|
||||||
|
const list = lists.get(listId);
|
||||||
|
if (!list) continue;
|
||||||
|
|
||||||
|
list.count = json.lists[listId].count;
|
||||||
|
|
||||||
|
for (const op of json.lists[listId].ops) {
|
||||||
|
switch (op.op) {
|
||||||
|
case "SYNC":
|
||||||
|
list.rooms.splice(op.range[0], op.range[1] - op.range[0] + 1, ...op.room_ids.map(id => rooms.get(id)!));
|
||||||
|
break;
|
||||||
|
// case "INVALIDATE":
|
||||||
|
// list.rooms.splice(op.range[0], op.range[1] - op.range[0] + 1);
|
||||||
|
// break;
|
||||||
|
default:
|
||||||
|
// the entire op system should probably be reworked
|
||||||
|
// if there's no sorting, i only need `sync` and `invalidate`?
|
||||||
|
throw new Error("unimplemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
private refresh() {
|
||||||
|
this.controller.abort("update query");
|
||||||
|
this.controller = new AbortController();
|
||||||
|
}
|
||||||
|
|
||||||
|
private stripSticky() {
|
||||||
|
const { query } = this;
|
||||||
|
const { lists } = query;
|
||||||
|
|
||||||
|
if (lists) {
|
||||||
|
for (const list in lists) {
|
||||||
|
lists[list] = { ranges: lists[list]!.ranges };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query.queries = {};
|
||||||
|
query.rooms = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
listSubscribe(name: string, subscription: ListSubscription) {
|
||||||
|
if (!this.query.lists) this.query.lists = {};
|
||||||
|
this.query.lists[name] = subscription;
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
listUnsubscribe(name: string) {
|
||||||
|
if (!this.query.lists) return;
|
||||||
|
delete this.query.lists[name];
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
roomSubscribe(roomId: string, subscription: RoomSubscription) {
|
||||||
|
if (!this.query.rooms) this.query.rooms = {};
|
||||||
|
this.query.rooms[roomId] = subscription;
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
roomUnsubscribe(roomId: string) {
|
||||||
|
if (!this.query.rooms) this.query.rooms = {};
|
||||||
|
this.query.rooms[roomId] = null;
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
abort(reason?: string) {
|
||||||
|
this.controller.abort(reason);
|
||||||
|
}
|
||||||
|
}
|
21
src/thread.ts
Normal file
21
src/thread.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import EventEmitter from "events";
|
||||||
|
import TypedEmitter from "typed-emitter";
|
||||||
|
import { Unreads } from "./api.js";
|
||||||
|
import { Room } from "./room.js";
|
||||||
|
import { Event } from "./event.js";
|
||||||
|
|
||||||
|
type ThreadEvents = {
|
||||||
|
// an event is appended to this thread's live timeline
|
||||||
|
timeline: (event: Event) => void,
|
||||||
|
|
||||||
|
// notifications for this thread were updated
|
||||||
|
notifications: (notifs: Unreads) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Thread extends (EventEmitter as unknown as new () => TypedEmitter<ThreadEvents>) {
|
||||||
|
public room: Room = this.baseEvent.room;
|
||||||
|
|
||||||
|
constructor(public baseEvent: Event) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
89
src/timeline.ts
Normal file
89
src/timeline.ts
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
// Timelines are ordered sequences of events.
|
||||||
|
|
||||||
|
// import TypedEventEmitter from "typed-emitter";
|
||||||
|
import { EventId } from "./api.js";
|
||||||
|
import { Room } from "./room.js";
|
||||||
|
import { Event } from "./event.js";
|
||||||
|
import { Thread } from "./thread.js";
|
||||||
|
|
||||||
|
interface Timeline {
|
||||||
|
isLive: boolean,
|
||||||
|
events: Array<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RoomTimeline implements Timeline {
|
||||||
|
public events: Array<Event> = [];
|
||||||
|
public isLive: boolean = false;
|
||||||
|
|
||||||
|
prevBatch: string | null = null;
|
||||||
|
nextBatch: string | null = null;
|
||||||
|
|
||||||
|
constructor(public room: Room) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// concat(other: RoomTimeline): RoomTimeline {
|
||||||
|
// if (this.events.length === 0) return other;
|
||||||
|
// if (other.events.length === 0) return this;
|
||||||
|
// const lastId = this.events.at(-1)!.id;
|
||||||
|
// const idx = other.events.findIndex((ev) => ev.id === lastId);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ThreadTimeline implements Timeline {
|
||||||
|
public events = [];
|
||||||
|
public isLive: boolean = false;
|
||||||
|
|
||||||
|
prevBatch: string | null = null;
|
||||||
|
nextBatch: string | null = null;
|
||||||
|
|
||||||
|
constructor(public thread: Thread) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TimelineSet {
|
||||||
|
// Other timelines *may* be live, but this one is guaranteed to be live
|
||||||
|
public live: RoomTimeline;
|
||||||
|
private timelines: Set<Timeline> = new Set();
|
||||||
|
// private eventIdToTimeline: Map<EventId, Timeline> = new Map();
|
||||||
|
|
||||||
|
constructor(public room: Room) {
|
||||||
|
this.live = new RoomTimeline(room);
|
||||||
|
this.live.isLive = true;
|
||||||
|
this.timelines.add(this.live);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a timeline for a thread. Becomes a live timeline if `atEnd = true`.
|
||||||
|
// public async forThread(eventId: EventId, atEnd: boolean): Promise<ThreadTimeline> {
|
||||||
|
// throw "todo";
|
||||||
|
// // const thread = new Thread();
|
||||||
|
// // const tl = new ThreadTimeline(thread);
|
||||||
|
// // return tl;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Get a timeline for an event (context).
|
||||||
|
public async forEvent(eventId: EventId): Promise<RoomTimeline> {
|
||||||
|
const context = await this.room.client.net.fetchContext(this.room.id, eventId);
|
||||||
|
const tl = new RoomTimeline(this.room);
|
||||||
|
tl.events = context.events_before
|
||||||
|
.reverse()
|
||||||
|
.concat([context.event])
|
||||||
|
.concat(context.events_after)
|
||||||
|
.map(raw => new Event(this.room, raw));
|
||||||
|
tl.prevBatch = context.start;
|
||||||
|
tl.nextBatch = context.end;
|
||||||
|
this.timelines.add(tl);
|
||||||
|
return tl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paginate a timeline for more events
|
||||||
|
// public paginate(timeline: Timeline, dir: "f" | "b", limit: number = 50) {
|
||||||
|
// throw "todo";
|
||||||
|
// }
|
||||||
|
|
||||||
|
_appendEvents(events: Array<Event>) {
|
||||||
|
// FIXME: merge timelines together
|
||||||
|
for (const timeline of this.timelines) {
|
||||||
|
if (timeline.isLive) timeline.events.push(...events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
src/utils.ts
Normal file
1
src/utils.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
// Various useful utilities
|
|
@ -3,7 +3,7 @@
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"outDir": "dist/",
|
"outDir": "dist/",
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true,
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue