-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
implement a class to access the irclog couchdb api
- Loading branch information
Showing
7 changed files
with
313 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,22 @@ | ||
# `irclog-api` | ||
|
||
Typescript API to access the IrcLog CouchDB database. Mostly typed wrappers over the http api of CouchDB. | ||
|
||
|
||
## Quick start for users | ||
|
||
``` | ||
TBD | ||
``` | ||
|
||
## Quick start for developers | ||
|
||
TBD | ||
``` | ||
pnpm install | ||
pnpm dev | ||
``` | ||
|
||
## References | ||
|
||
- https://docs.couchdb.org/en/stable/api/ddoc/views.html#db-design-design-doc-view-view-name |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
/* | ||
* Low level wrappers over fetch to work with couchdb | ||
* | ||
*/ | ||
|
||
import { Channel, Message, Query, ViewResponse, MessageViewResponse } from './types'; | ||
import { postQuery } from './post-query'; | ||
|
||
const commonQueryArgs = { | ||
include_docs: true, | ||
update_seq: true, | ||
reduce: false, | ||
}; | ||
|
||
// hard-coded based on the Couch Application deployed | ||
const COUCH_DESIGN_DOC = '_design/log/_view/channel'; | ||
const COUCH_URL = 'https://db.softver.org.mk/irclog/'; | ||
|
||
/** | ||
* FIXME: docs | ||
*/ | ||
export class CouchDB { | ||
#couchUrl: URL; | ||
#designDoc: string; | ||
#queryUrl: URL; | ||
|
||
constructor(couchUrl: string, designDoc?: string) { | ||
this.#couchUrl = new URL(couchUrl); | ||
this.#designDoc = designDoc ?? COUCH_DESIGN_DOC; | ||
this.#queryUrl = new URL(this.#designDoc, this.#couchUrl); | ||
} | ||
|
||
async fetchViewLatest(channel: string, limit = 100): Promise<MessageViewResponse> { | ||
const query: Query = { | ||
...commonQueryArgs, | ||
limit: limit, | ||
descending: true, | ||
startkey: [channel, {}], | ||
endkey: [channel, 0], | ||
}; | ||
|
||
// FIXME: validation needed here | ||
const page = await postQuery(this.#queryUrl, query); | ||
page.rows.reverse(); | ||
page.rows = page.rows.map((row: { doc: Message }) => row.doc); | ||
return page; | ||
} | ||
|
||
async fetchViewAtTimestamp(channel: string, timestamp: number, limit: number): Promise<MessageViewResponse> { | ||
const query: Query = { | ||
...commonQueryArgs, | ||
limit: limit, | ||
descending: false, | ||
startkey: [channel, timestamp], | ||
endkey: [channel, {}], | ||
}; | ||
|
||
// FIXME: validation needed here | ||
const page = await postQuery(this.#queryUrl, query); | ||
page.rows = page.rows.map((row: { doc: Message }) => row.doc); | ||
return page; | ||
} | ||
|
||
async fetchViewBefore(channel: string, firstRow: Message, limit: number): Promise<MessageViewResponse> { | ||
const query: Query = { | ||
...commonQueryArgs, | ||
limit: limit, | ||
descending: true, | ||
skip: 1, | ||
startkey: [channel, firstRow.timestamp], | ||
startkey_docid: firstRow._id, | ||
endkey: [channel, 0], | ||
}; | ||
|
||
// FIXME: validation needed here | ||
const view = await postQuery(this.#queryUrl, query); | ||
view.rows.reverse(); | ||
view.rows = view.rows.map((row: { doc: Message }) => row.doc); | ||
return view; | ||
} | ||
|
||
async fetchViewAfter(channel: string, lastRow: Message, limit: number): Promise<MessageViewResponse> { | ||
const query: Query = { | ||
...commonQueryArgs, | ||
limit: limit, | ||
descending: false, | ||
skip: 1, | ||
startkey: [channel, lastRow.timestamp], | ||
startkey_docid: lastRow._id, | ||
endkey: [channel, {}], | ||
}; | ||
|
||
// FIXME: validation needed here | ||
const view = await postQuery(this.#queryUrl, query); | ||
view.rows = view.rows.map((row: { doc: Message }) => row.doc); | ||
return view; | ||
} | ||
|
||
async fetchChannelList(): Promise<Channel[]> { | ||
const query: Query = { | ||
update_seq: true, | ||
reduce: true, | ||
group_level: 1, | ||
include_docs: false, | ||
}; | ||
|
||
// FIXME: validation needed here | ||
const chanList = (await postQuery(this.#queryUrl, query)) as ViewResponse<GroupLevel1Row>; | ||
return chanList.rows?.map(extractChannelData) ?? []; | ||
} | ||
} | ||
|
||
type GroupLevel1Row = { key: [string]; value: number }; | ||
|
||
function extractChannelData(row: GroupLevel1Row): Channel { | ||
return { channelName: row.key[0], totalMessages: row.value }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { Message } from './types'; | ||
|
||
export type ChangesResponse = { | ||
results: { doc: Message }[]; | ||
last_seq: string; | ||
}; | ||
|
||
export async function fetchFeed( | ||
couchUrl: URL, | ||
channel: string, | ||
since: string, | ||
signal?: AbortSignal, | ||
): Promise<ChangesResponse> { | ||
const feedUrl = new URL('_changes', couchUrl); | ||
const query = { | ||
feed: 'longpoll', | ||
timeout: '90000', | ||
include_docs: 'true', | ||
filter: '_selector', | ||
since: since, | ||
}; | ||
feedUrl.search = new URLSearchParams(query).toString(); | ||
|
||
const options: RequestInit = { | ||
signal, | ||
mode: 'cors', | ||
headers: { | ||
accept: 'application/json', | ||
'content-type': 'application/json', | ||
}, | ||
body: JSON.stringify({ selector: { channel: channel } }), | ||
method: 'POST', | ||
}; | ||
const response = await fetch(`${feedUrl}`, options); | ||
if (!response.ok) { | ||
throw new Error(`Network response was not ok: ${response.statusText}`); | ||
} | ||
return await response.json(); | ||
} | ||
|
||
/* | ||
* Experimental Feed using the Server-Sent-Events stream from CouchDB | ||
*/ | ||
|
||
function parseSseEvent(data: string) { | ||
const tmp: Record<string, string> = {}; | ||
const lines = data.split(/\n/); | ||
for (const line of lines) { | ||
if (line) { | ||
const [key, value] = line.split(/: /, 2); | ||
tmp[key] = value; | ||
} | ||
} | ||
return new MessageEvent<string>('message', tmp); | ||
} | ||
|
||
function asEventSourceStream() { | ||
let buffer: string; | ||
const transformer: Transformer<string, MessageEvent<string>> = { | ||
start() { | ||
buffer = ''; | ||
}, | ||
transform(chunk, controller) { | ||
buffer += chunk; | ||
while (true) { | ||
const [event, sep, rest] = buffer.split(/(\n\n)/, 3); | ||
if (sep !== '\n\n') break; | ||
buffer = rest; | ||
controller.enqueue(parseSseEvent(event)); | ||
if (rest === '') break; | ||
} | ||
}, | ||
}; | ||
return new TransformStream(transformer); | ||
} | ||
|
||
export async function fetchEventStream(url: URL, heartbeat?: number, init?: RequestInit) { | ||
const heartbeat_ = heartbeat ?? 60000; | ||
|
||
const headers = { ...init?.headers, Accept: 'text/event-stream' }; | ||
const u = new URL(url); | ||
u.searchParams.set('feed', 'eventsource'); | ||
u.searchParams.set('heartbeat', `${heartbeat_}`); | ||
u.searchParams.set('since', 'now'); | ||
|
||
const controller = new AbortController(); | ||
const signal = controller.signal; | ||
|
||
const resp = await fetch(u, { ...init, headers, signal }); | ||
if (!resp.ok) return; | ||
|
||
const reader = resp.body!.pipeThrough(new TextDecoderStream()).pipeThrough(asEventSourceStream()).getReader(); | ||
|
||
while (true) { | ||
let timeoutID = setTimeout(() => controller.abort(), heartbeat_ * 2); | ||
const { done, value } = await reader.read(); | ||
clearTimeout(timeoutID); | ||
if (done) break; | ||
console.log(value); | ||
} | ||
console.log('Stream done'); | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
export { sayHello, sayGoodbye } from './hello-world'; | ||
export { CouchDB } from './couch-api'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { Query } from './types'; | ||
|
||
/** | ||
* Helper function to send a query to the irc log CouchDB View using an http post request | ||
* | ||
* https://docs.couchdb.org/en/stable/api/ddoc/views.html#querying-views-and-indexes | ||
* https://docs.couchdb.org/en/stable/ddocs/views/intro.html | ||
* | ||
* @param url | ||
* @param query | ||
* @returns A Fetch API Response - query results | ||
*/ | ||
export async function postQuery(url: URL, query: Query) { | ||
const options: RequestInit = { | ||
mode: 'cors', | ||
headers: { | ||
accept: 'application/json', | ||
'content-type': 'application/json', | ||
}, | ||
method: 'POST', | ||
}; | ||
|
||
const response = await fetch(url, { | ||
...options, | ||
body: JSON.stringify(query), | ||
}); | ||
|
||
if (!response.ok) { | ||
throw new Error(`Network response was not ok: ${response.statusText}`); | ||
} | ||
return response.json(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
export type Channel = { | ||
channelName: string; | ||
totalMessages: number; | ||
}; | ||
|
||
export type Message = { | ||
timestamp: number; | ||
sender: string; | ||
channel: string; | ||
message: string; | ||
_id: string; | ||
}; | ||
|
||
// TODO: map all from here: https://docs.couchdb.org/en/stable/api/ddoc/views.html#db-design-design-doc-view-view-name | ||
export type Query = { | ||
include_docs: boolean; | ||
update_seq: boolean; | ||
reduce: boolean; | ||
group_level?: number; | ||
limit?: number; | ||
skip?: number; | ||
descending?: boolean; | ||
startkey?: any; | ||
endkey?: any; | ||
startkey_docid?: any; | ||
endkey_docid?: any; | ||
// [key: string]: any; | ||
}; | ||
|
||
// CouchDB Result schema | ||
export type ViewResponse<TRow> = { | ||
rows: TRow[]; | ||
update_seq: string; | ||
total_rows?: number; | ||
offset?: number; | ||
}; | ||
|
||
export type MessageViewResponse = { | ||
channel: string; | ||
} & ViewResponse<Message>; |