feature: events polling

This commit is contained in:
Andy Burke 2025-07-02 21:28:07 -07:00
parent 61a51017a3
commit b700251278
19 changed files with 353 additions and 77 deletions

21
.zed/settings.json Normal file
View file

@ -0,0 +1,21 @@
{
"lsp": {
"deno": {
"settings": {
"deno": {
"enable": true
}
}
}
},
"languages": {
"TypeScript": {
"language_servers": ["deno", "!typescript-language-server", "!vtsls", "!eslint"],
"formatter": "language_server"
},
"TSX": {
"language_servers": ["deno", "!typescript-language-server", "!vtsls", "!eslint"],
"formatter": "language_server"
}
}
}

View file

@ -32,13 +32,13 @@
} }
}, },
"imports": { "imports": {
"@andyburke/fsdb": "jsr:@andyburke/fsdb@^0.6.1", "@andyburke/fsdb": "jsr:@andyburke/fsdb@^0.9.0",
"@andyburke/lurid": "jsr:@andyburke/lurid@^0.2.0", "@andyburke/lurid": "jsr:@andyburke/lurid@^0.2.0",
"@andyburke/serverus": "jsr:@andyburke/serverus@^0.7.1", "@andyburke/serverus": "jsr:@andyburke/serverus@^0.7.1",
"@std/assert": "jsr:@std/assert@^1.0.13", "@std/assert": "jsr:@std/assert@^1.0.13",
"@std/encoding": "jsr:@std/encoding@^1.0.10", "@std/encoding": "jsr:@std/encoding@^1.0.10",
"@std/http": "jsr:@std/http@^1.0.18", "@std/http": "jsr:@std/http@^1.0.19",
"@std/path": "jsr:@std/path@^1.1.0", "@std/path": "jsr:@std/path@^1.1.1",
"@stdext/crypto": "jsr:@stdext/crypto@^0.1.0" "@stdext/crypto": "jsr:@stdext/crypto@^0.1.0"
} }
} }

63
deno.lock generated
View file

@ -1,9 +1,11 @@
{ {
"version": "5", "version": "5",
"specifiers": { "specifiers": {
"jsr:@andyburke/fsdb@~0.6.1": "0.6.1", "jsr:@andyburke/fsdb@*": "0.9.0",
"jsr:@andyburke/fsdb@0.9": "0.9.0",
"jsr:@andyburke/lurid@*": "0.2.0", "jsr:@andyburke/lurid@*": "0.2.0",
"jsr:@andyburke/lurid@0.2": "0.2.0", "jsr:@andyburke/lurid@0.2": "0.2.0",
"jsr:@andyburke/serverus@*": "0.7.1",
"jsr:@andyburke/serverus@~0.7.1": "0.7.1", "jsr:@andyburke/serverus@~0.7.1": "0.7.1",
"jsr:@std/assert@*": "1.0.13", "jsr:@std/assert@*": "1.0.13",
"jsr:@std/assert@^1.0.13": "1.0.13", "jsr:@std/assert@^1.0.13": "1.0.13",
@ -15,24 +17,29 @@
"jsr:@std/encoding@^1.0.10": "1.0.10", "jsr:@std/encoding@^1.0.10": "1.0.10",
"jsr:@std/fmt@^1.0.6": "1.0.8", "jsr:@std/fmt@^1.0.6": "1.0.8",
"jsr:@std/fmt@^1.0.8": "1.0.8", "jsr:@std/fmt@^1.0.8": "1.0.8",
"jsr:@std/fs@^1.0.14": "1.0.18", "jsr:@std/fs@^1.0.14": "1.0.19",
"jsr:@std/fs@^1.0.18": "1.0.18", "jsr:@std/fs@^1.0.18": "1.0.19",
"jsr:@std/fs@^1.0.19": "1.0.19",
"jsr:@std/html@^1.0.4": "1.0.4", "jsr:@std/html@^1.0.4": "1.0.4",
"jsr:@std/http@*": "1.0.18", "jsr:@std/http@*": "1.0.18",
"jsr:@std/http@^1.0.13": "1.0.18", "jsr:@std/http@^1.0.13": "1.0.19",
"jsr:@std/http@^1.0.18": "1.0.18", "jsr:@std/http@^1.0.18": "1.0.18",
"jsr:@std/internal@^1.0.6": "1.0.8", "jsr:@std/http@^1.0.19": "1.0.19",
"jsr:@std/internal@^1.0.6": "1.0.9",
"jsr:@std/internal@^1.0.9": "1.0.9",
"jsr:@std/media-types@^1.1.0": "1.1.0", "jsr:@std/media-types@^1.1.0": "1.1.0",
"jsr:@std/net@^1.0.4": "1.0.4", "jsr:@std/net@^1.0.4": "1.0.4",
"jsr:@std/path@^1.0.8": "1.1.0", "jsr:@std/path@*": "1.1.1",
"jsr:@std/path@^1.1.0": "1.1.0", "jsr:@std/path@^1.0.8": "1.1.1",
"jsr:@std/path@^1.1.0": "1.1.1",
"jsr:@std/path@^1.1.1": "1.1.1",
"jsr:@std/streams@^1.0.10": "1.0.10", "jsr:@std/streams@^1.0.10": "1.0.10",
"jsr:@stdext/crypto@*": "0.1.0", "jsr:@stdext/crypto@*": "0.1.0",
"jsr:@stdext/crypto@0.1": "0.1.0" "jsr:@stdext/crypto@0.1": "0.1.0"
}, },
"jsr": { "jsr": {
"@andyburke/fsdb@0.6.1": { "@andyburke/fsdb@0.9.0": {
"integrity": "059ad6702e40a39a188e648a8ebf2547087782becae040af916aa843830328ea", "integrity": "726c138ac8b751c969cb045d9d1fc3541a054130aa741d256864b376afdb8f89",
"dependencies": [ "dependencies": [
"jsr:@std/cli@^1.0.20", "jsr:@std/cli@^1.0.20",
"jsr:@std/fs@^1.0.18", "jsr:@std/fs@^1.0.18",
@ -60,7 +67,7 @@
"@std/assert@1.0.13": { "@std/assert@1.0.13": {
"integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29", "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29",
"dependencies": [ "dependencies": [
"jsr:@std/internal" "jsr:@std/internal@^1.0.6"
] ]
}, },
"@std/async@1.0.13": { "@std/async@1.0.13": {
@ -81,6 +88,13 @@
"jsr:@std/path@^1.1.0" "jsr:@std/path@^1.1.0"
] ]
}, },
"@std/fs@1.0.19": {
"integrity": "051968c2b1eae4d2ea9f79a08a3845740ef6af10356aff43d3e2ef11ed09fb06",
"dependencies": [
"jsr:@std/internal@^1.0.9",
"jsr:@std/path@^1.1.1"
]
},
"@std/html@1.0.4": { "@std/html@1.0.4": {
"integrity": "eff3497c08164e6ada49b7f81a28b5108087033823153d065e3f89467dd3d50e" "integrity": "eff3497c08164e6ada49b7f81a28b5108087033823153d065e3f89467dd3d50e"
}, },
@ -101,9 +115,26 @@
"jsr:@std/streams" "jsr:@std/streams"
] ]
}, },
"@std/http@1.0.19": {
"integrity": "52128c8d00a1f0b20019f8b72376e7ef5f3133375b6f805b5bc89b9de2ad4686",
"dependencies": [
"jsr:@std/cli@^1.0.20",
"jsr:@std/encoding@^1.0.10",
"jsr:@std/fmt@^1.0.8",
"jsr:@std/fs@^1.0.19",
"jsr:@std/html",
"jsr:@std/media-types",
"jsr:@std/net",
"jsr:@std/path@^1.1.1",
"jsr:@std/streams"
]
},
"@std/internal@1.0.8": { "@std/internal@1.0.8": {
"integrity": "fc66e846d8d38a47cffd274d80d2ca3f0de71040f855783724bb6b87f60891f5" "integrity": "fc66e846d8d38a47cffd274d80d2ca3f0de71040f855783724bb6b87f60891f5"
}, },
"@std/internal@1.0.9": {
"integrity": "bdfb97f83e4db7a13e8faab26fb1958d1b80cc64366501af78a0aee151696eb8"
},
"@std/media-types@1.1.0": { "@std/media-types@1.1.0": {
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
}, },
@ -113,6 +144,12 @@
"@std/path@1.1.0": { "@std/path@1.1.0": {
"integrity": "ddc94f8e3c275627281cbc23341df6b8bcc874d70374f75fec2533521e3d6886" "integrity": "ddc94f8e3c275627281cbc23341df6b8bcc874d70374f75fec2533521e3d6886"
}, },
"@std/path@1.1.1": {
"integrity": "fe00026bd3a7e6a27f73709b83c607798be40e20c81dde655ce34052fd82ec76",
"dependencies": [
"jsr:@std/internal@^1.0.9"
]
},
"@std/streams@1.0.10": { "@std/streams@1.0.10": {
"integrity": "75c0b1431873cd0d8b3d679015220204d36d3c7420d93b60acfc379eb0dc30af" "integrity": "75c0b1431873cd0d8b3d679015220204d36d3c7420d93b60acfc379eb0dc30af"
}, },
@ -165,13 +202,13 @@
}, },
"workspace": { "workspace": {
"dependencies": [ "dependencies": [
"jsr:@andyburke/fsdb@~0.6.1", "jsr:@andyburke/fsdb@0.9",
"jsr:@andyburke/lurid@0.2", "jsr:@andyburke/lurid@0.2",
"jsr:@andyburke/serverus@~0.7.1", "jsr:@andyburke/serverus@~0.7.1",
"jsr:@std/assert@^1.0.13", "jsr:@std/assert@^1.0.13",
"jsr:@std/encoding@^1.0.10", "jsr:@std/encoding@^1.0.10",
"jsr:@std/http@^1.0.18", "jsr:@std/http@^1.0.19",
"jsr:@std/path@^1.1.0", "jsr:@std/path@^1.1.1",
"jsr:@stdext/crypto@0.1" "jsr:@stdext/crypto@0.1"
] ]
} }

View file

@ -11,7 +11,7 @@ import { FSDB_INDEXER_SYMLINKS } from 'jsr:@andyburke/fsdb/indexers';
/** /**
* Event * Event
* *
* @property {string} id - room_id(lurid):event_id(lurid) * @property {string} id - lurid
* @property {string} creator_id - id of the source user * @property {string} creator_id - id of the source user
* @property {string} room_id - id of the target room * @property {string} room_id - id of the target room
* @property {string} type - event type * @property {string} type - event type
@ -31,28 +31,55 @@ export type EVENT = {
}; };
}; };
export const EVENTS = new FSDB_COLLECTION<EVENT>({ type ROOM_EVENT_CACHE_ENTRY = {
name: 'events', collection: FSDB_COLLECTION<EVENT>;
id_field: 'id', eviction_timeout: number;
organize: (combined_id: string) => { };
const [room_id, event_id] = combined_id.split(':', 2);
return ['rooms', room_id, event_id.substring(0, 14), `${event_id}.json`];
},
indexers: {
creator_id: new FSDB_INDEXER_SYMLINKS<EVENT>({
name: 'creator_id',
field: 'creator_id',
to_many: true,
organize: by_lurid
}),
tags: new FSDB_INDEXER_SYMLINKS<EVENT>({ const ROOM_EVENTS: Record<string, ROOM_EVENT_CACHE_ENTRY> = {};
name: 'tags', export function get_events_collection_for_room(room_id: string): FSDB_COLLECTION<EVENT> {
get_values_to_index: (event: EVENT): string[] => { ROOM_EVENTS[room_id] = ROOM_EVENTS[room_id] ?? {
return (event.tags ?? []).map((tag: string) => tag.toLowerCase()); collection: new FSDB_COLLECTION<EVENT>({
}, name: `rooms/${room_id.substring(0, 14)}/${room_id.substring(0, 36)}/${room_id}/events`,
to_many: true, id_field: 'id',
organize: by_character organize: by_lurid,
}) indexers: {
creator_id: new FSDB_INDEXER_SYMLINKS<EVENT>({
name: 'creator_id',
field: 'creator_id',
to_many: true,
organize: by_lurid
}),
tags: new FSDB_INDEXER_SYMLINKS<EVENT>({
name: 'tags',
get_values_to_index: (event: EVENT): string[] => {
return (event.tags ?? []).map((tag: string) => tag.toLowerCase());
},
to_many: true,
organize: by_character
})
}
}),
eviction_timeout: 0
};
if (ROOM_EVENTS[room_id].eviction_timeout) {
clearTimeout(ROOM_EVENTS[room_id].eviction_timeout);
} }
});
ROOM_EVENTS[room_id].eviction_timeout = setTimeout(() => {
delete ROOM_EVENTS[room_id];
}, 60_000 * 5);
return ROOM_EVENTS[room_id].collection;
}
export function clear_room_events_cache() {
for (const [room_id, cached] of Object.entries(ROOM_EVENTS)) {
if (cached.eviction_timeout) {
clearTimeout(cached.eviction_timeout);
}
delete ROOM_EVENTS[room_id];
}
}

View file

@ -53,7 +53,7 @@ export async function POST(req: Request, meta: Record<string, any>): Promise<Res
let user: USER | undefined = undefined; let user: USER | undefined = undefined;
user = (await USERS.find({ user = (await USERS.find({
username username
})).shift(); })).shift()?.load();
if (!user) { if (!user) {
return Response.json({ return Response.json({

View file

@ -1,4 +1,5 @@
import { EVENT, EVENTS } from '../../../../../../models/event.ts'; import { FSDB_COLLECTION } from '@andyburke/fsdb';
import { EVENT, get_events_collection_for_room } from '../../../../../../models/event.ts';
import { ROOM, ROOMS } from '../../../../../../models/room.ts'; import { ROOM, ROOMS } from '../../../../../../models/room.ts';
import parse_body from '../../../../../../utils/bodyparser.ts'; import parse_body from '../../../../../../utils/bodyparser.ts';
import * as CANNED_RESPONSES from '../../../../../../utils/canned_responses.ts'; import * as CANNED_RESPONSES from '../../../../../../utils/canned_responses.ts';
@ -29,7 +30,8 @@ PRECHECKS.GET = [get_session, get_user, require_user, async (_req: Request, meta
} }
}]; }];
export async function GET(_req: Request, meta: Record<string, any>): Promise<Response> { export async function GET(_req: Request, meta: Record<string, any>): Promise<Response> {
const event: EVENT | null = await EVENTS.get(meta.params.event_id); const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_room(meta.room.id);
const event: EVENT | null = await events.get(meta.params.event_id);
if (!event) { if (!event) {
return CANNED_RESPONSES.not_found(); return CANNED_RESPONSES.not_found();
@ -76,7 +78,8 @@ export async function PUT(req: Request, meta: Record<string, any>): Promise<Resp
const now = new Date().toISOString(); const now = new Date().toISOString();
try { try {
const event: EVENT | null = await EVENTS.get(meta.params.event_id); const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_room(meta.room.id);
const event: EVENT | null = await events.get(meta.params.event_id);
if (!event) { if (!event) {
return CANNED_RESPONSES.not_found(); return CANNED_RESPONSES.not_found();
@ -98,7 +101,7 @@ export async function PUT(req: Request, meta: Record<string, any>): Promise<Resp
} }
}; };
await EVENTS.update(updated); await events.update(updated);
return Response.json(updated, { return Response.json(updated, {
status: 200 status: 200
}); });
@ -147,12 +150,13 @@ PRECHECKS.DELETE = [
} }
]; ];
export async function DELETE(_req: Request, meta: Record<string, any>): Promise<Response> { export async function DELETE(_req: Request, meta: Record<string, any>): Promise<Response> {
const event: EVENT | null = await EVENTS.get(meta.params.event_id); const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_room(meta.room.id);
const event: EVENT | null = await events.get(meta.params.event_id);
if (!event) { if (!event) {
return CANNED_RESPONSES.not_found(); return CANNED_RESPONSES.not_found();
} }
await EVENTS.delete(event); await events.delete(event);
return Response.json({ return Response.json({
deleted: true deleted: true

View file

@ -2,8 +2,10 @@ import lurid from 'jsr:@andyburke/lurid';
import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../../../utils/prechecks.ts'; import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../../../utils/prechecks.ts';
import { ROOM, ROOMS } from '../../../../../models/room.ts'; import { ROOM, ROOMS } from '../../../../../models/room.ts';
import * as CANNED_RESPONSES from '../../../../../utils/canned_responses.ts'; import * as CANNED_RESPONSES from '../../../../../utils/canned_responses.ts';
import { EVENT, EVENTS } from '../../../../../models/event.ts'; import { EVENT, get_events_collection_for_room } from '../../../../../models/event.ts';
import parse_body from '../../../../../utils/bodyparser.ts'; import parse_body from '../../../../../utils/bodyparser.ts';
import { FSDB_COLLECTION, FSDB_SEARCH_OPTIONS, WALK_ENTRY } from 'jsr:@andyburke/fsdb';
import * as path from 'jsr:@std/path';
export const PRECHECKS: PRECHECK_TABLE = {}; export const PRECHECKS: PRECHECK_TABLE = {};
@ -31,30 +33,80 @@ PRECHECKS.GET = [get_session, get_user, require_user, async (_req: Request, meta
return CANNED_RESPONSES.permission_denied(); return CANNED_RESPONSES.permission_denied();
} }
}]; }];
export async function GET(_req: Request, meta: Record<string, any>): Promise<Response> { export async function GET(request: Request, meta: Record<string, any>): Promise<Response> {
const query: URLSearchParams = meta.query; const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_room(meta.room.id);
const partial_id: string | undefined = query.get('partial_id')?.toLowerCase().trim();
const has_partial_id = typeof partial_id === 'string' && partial_id.length >= 2; const sorts = events.sorts;
if (!has_partial_id) { const sort_name: string = meta.query.sort ?? 'newest';
const key = sort_name as keyof typeof sorts;
const sort: any = sorts[key];
if (!sort) {
return Response.json({ return Response.json({
error: { error: {
message: 'You must specify a `partial_id` query parameter.', message: 'You must specify a sort: newest, oldest, latest, stalest',
cause: 'missing_query_parameter' cause: 'invalid_sort'
} }
}, { }, {
status: 400 status: 400
}); });
} }
const limit = Math.min(parseInt(query.get('limit') ?? '10'), 100); const options: FSDB_SEARCH_OPTIONS<EVENT> = {
const events = await EVENTS.all({ ...(meta.query ?? {}),
id_after: partial_id, limit: Math.min(parseInt(meta.query?.limit ?? '10'), 1_000),
limit sort,
}); filter: (entry: WALK_ENTRY<EVENT>) => {
const event_id = path.basename(entry.path).replace(/\.json$/i, '');
return Response.json(events, { if (meta.query.after_id && event_id <= meta.query.after_id) {
status: 200 return false;
}
if (meta.query.before_id && event_id >= meta.query.before_id) {
return false;
}
return true;
}
};
const headers = {
'Cache-Control': 'no-cache, must-revalidate'
};
const results = (await events.all(options)).map((entry) => entry.load());
// long-polling support
if (results.length === 0 && meta.query.wait) {
return new Promise((resolve) => {
function on_create(create_event: any) {
results.push(create_event.item);
clearTimeout(timeout);
events.off('create', on_create);
return resolve(Response.json(results, {
status: 200,
headers
}));
}
const timeout = setTimeout(() => {
events.off('create', on_create);
return resolve(Response.json(results, {
status: 200,
headers
}));
}, 60_000); // 60 seconds
events.on('create', on_create);
request.signal.addEventListener('abort', () => {
events.off('create', on_create);
});
});
}
return Response.json(results, {
status: 200,
headers
}); });
} }
@ -82,12 +134,15 @@ PRECHECKS.POST = [get_session, get_user, require_user, async (_req: Request, met
}]; }];
export async function POST(req: Request, meta: Record<string, any>): Promise<Response> { export async function POST(req: Request, meta: Record<string, any>): Promise<Response> {
try { try {
const events: FSDB_COLLECTION<EVENT> = get_events_collection_for_room(meta.room.id);
const now = new Date().toISOString(); const now = new Date().toISOString();
const body = await parse_body(req); const body = await parse_body(req);
const new_event: EVENT = { const event: EVENT = {
type: 'unknown', type: 'unknown',
...body, ...body,
id: `${meta.params.room_id}:${lurid()}`, id: lurid(),
creator_id: meta.user.id, creator_id: meta.user.id,
timestamps: { timestamps: {
created: now, created: now,
@ -95,9 +150,9 @@ export async function POST(req: Request, meta: Record<string, any>): Promise<Res
} }
}; };
await EVENTS.create(new_event); await events.create(event);
return Response.json(new_event, { return Response.json(event, {
status: 201 status: 201
}); });
} catch (error) { } catch (error) {

View file

@ -134,11 +134,11 @@ export async function DELETE(_req: Request, meta: Record<string, any>): Promise<
await PASSWORD_ENTRIES.delete(password_entry); await PASSWORD_ENTRIES.delete(password_entry);
} }
const sessions = await SESSIONS.find({ const session_entries = await SESSIONS.find({
user_id user_id
}); });
for (const session of sessions) { for (const entry of session_entries) {
await SESSIONS.delete(session); await SESSIONS.delete(entry.load());
} }
await USERS.delete(user); await USERS.delete(user);

View file

@ -1,8 +1,8 @@
/* Dark mode default */ /* Dark mode default */
:root { :root {
--bg: #121212; --bg: #323232;
--text: #f0f0f0; --text: #efe;
--accent: #4caf50; --accent: #fa0 ;
--border-subtle: #555; --border-subtle: #555;
--border-normal: #888; --border-normal: #888;
--border-highlight: #bbb; --border-highlight: #bbb;
@ -13,7 +13,7 @@
:root { :root {
--bg: #f0f0f0; --bg: #f0f0f0;
--text: #121212; --text: #121212;
--accent: #4caf50; --accent: #c80;
--border-subtle: #bbb; --border-subtle: #bbb;
--border-normal: #888; --border-normal: #888;
--border-highlight: #555; --border-highlight: #555;
@ -135,9 +135,8 @@ button {
background: inherit; background: inherit;
color: inherit; color: inherit;
padding: 0.5rem; padding: 0.5rem;
margin: 0 1rem;
border: 1px solid var(--text); border: 1px solid var(--text);
border-radius: 10%; border-radius: 4px;
} }
button.primary { button.primary {

View file

@ -150,7 +150,6 @@
document.body.dataset.user = JSON.stringify(response.user); document.body.dataset.user = JSON.stringify(response.user);
document.body.dataset.perms = document.body.dataset.perms =
response.user.permissions.join(":"); response.user.permissions.join(":");
console.dir({ response });
}; };
} }
</script> </script>

View file

@ -90,7 +90,7 @@
.tab-switch:checked + .tab-label { .tab-switch:checked + .tab-label {
margin-top: 1px; margin-top: 1px;
border-bottom: 1px solid var(--border-highlight); border-bottom: 1px solid var(--accent);
z-index: 1; z-index: 1;
} }

View file

@ -71,6 +71,7 @@
#talk #room-chat-entry-container form button { #talk #room-chat-entry-container form button {
width: inherit; width: inherit;
padding: inherit; padding: inherit;
margin: 0 1rem;
} }
#talk #room-chat-entry-container form textarea { #talk #room-chat-entry-container form textarea {

View file

@ -2,6 +2,7 @@ import { api, API_CLIENT } from '../../../utils/api.ts';
import * as asserts from 'jsr:@std/assert'; import * as asserts from 'jsr:@std/assert';
import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../helpers.ts'; import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../helpers.ts';
import { generateTotp } from '@stdext/crypto/totp'; import { generateTotp } from '@stdext/crypto/totp';
import { clear_room_events_cache } from '../../../models/event.ts';
Deno.test({ Deno.test({
name: 'API - ROOMS - Create', name: 'API - ROOMS - Create',
@ -72,6 +73,7 @@ Deno.test({
asserts.assert(new_room); asserts.assert(new_room);
} finally { } finally {
clear_room_events_cache();
if (test_server_info) { if (test_server_info) {
await test_server_info?.server?.stop(); await test_server_info?.server?.stop();
} }

View file

@ -2,6 +2,7 @@ import { api, API_CLIENT } from '../../../utils/api.ts';
import * as asserts from 'jsr:@std/assert'; import * as asserts from 'jsr:@std/assert';
import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../helpers.ts'; import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../helpers.ts';
import { generateTotp } from '@stdext/crypto/totp'; import { generateTotp } from '@stdext/crypto/totp';
import { clear_room_events_cache } from '../../../models/event.ts';
Deno.test({ Deno.test({
name: 'API - ROOMS - Delete', name: 'API - ROOMS - Delete',
@ -48,6 +49,7 @@ Deno.test({
asserts.assert(deleted_room); asserts.assert(deleted_room);
} finally { } finally {
clear_room_events_cache();
if (test_server_info) { if (test_server_info) {
await test_server_info?.server?.stop(); await test_server_info?.server?.stop();
} }

View file

@ -2,6 +2,7 @@ import * as asserts from 'jsr:@std/assert';
import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../../helpers.ts'; import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../../helpers.ts';
import { api, API_CLIENT } from '../../../../utils/api.ts'; import { api, API_CLIENT } from '../../../../utils/api.ts';
import { generateTotp } from '@stdext/crypto/totp'; import { generateTotp } from '@stdext/crypto/totp';
import { clear_room_events_cache } from '../../../../models/event.ts';
Deno.test({ Deno.test({
name: 'API - ROOMS - EVENTS - Create', name: 'API - ROOMS - EVENTS - Create',
@ -113,6 +114,7 @@ Deno.test({
asserts.assert(event_from_other_user); asserts.assert(event_from_other_user);
} finally { } finally {
clear_room_events_cache();
if (test_server_info) { if (test_server_info) {
await test_server_info?.server?.stop(); await test_server_info?.server?.stop();
} }

View file

@ -0,0 +1,121 @@
import * as asserts from 'jsr:@std/assert';
import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../../helpers.ts';
import { api, API_CLIENT } from '../../../../utils/api.ts';
import { generateTotp } from '@stdext/crypto/totp';
import { clear_room_events_cache } from '../../../../models/event.ts';
Deno.test({
name: 'API - ROOMS - EVENTS - Get',
permissions: {
env: true,
read: true,
write: true,
net: true
},
fn: async () => {
let test_server_info: EPHEMERAL_SERVER | null = null;
try {
test_server_info = await get_ephemeral_listen_server();
const client: API_CLIENT = api({
prefix: '/api',
hostname: test_server_info.hostname,
port: test_server_info.port
});
const owner_info = await get_new_user(client);
await set_user_permissions(client, owner_info.user, owner_info.session, [...owner_info.user.permissions, 'rooms.create']);
const room = await client.fetch('/rooms', {
method: 'POST',
headers: {
'x-session_id': owner_info.session.id,
'x-totp': await generateTotp(owner_info.session.secret)
},
json: {
name: 'test get events room'
}
});
asserts.assert(room);
const NUM_INITIAL_EVENTS = 5;
const events_initial_batch: any[] = [];
for (let i = 0; i < NUM_INITIAL_EVENTS; ++i) {
const event = await client.fetch(`/rooms/${room.id}/events`, {
method: 'POST',
headers: {
'x-session_id': owner_info.session.id,
'x-totp': await generateTotp(owner_info.session.secret)
},
json: {
type: 'test',
data: {
i
}
}
});
asserts.assert(event);
events_initial_batch.push(event);
}
asserts.assertEquals(events_initial_batch.length, NUM_INITIAL_EVENTS);
const other_user_info = await get_new_user(client);
const events_from_server = await client.fetch(`/rooms/${room.id}/events`, {
method: 'GET',
headers: {
'x-session_id': other_user_info.session.id,
'x-totp': await generateTotp(other_user_info.session.secret)
}
});
asserts.assertEquals(events_from_server.length, NUM_INITIAL_EVENTS);
const newest_event = events_from_server[0];
asserts.assert(newest_event);
const long_poll_request_promise = client.fetch(`/rooms/${room.id}/events?wait=true&after_id=${newest_event.id}`, {
method: 'GET',
headers: {
'x-session_id': other_user_info.session.id,
'x-totp': await generateTotp(other_user_info.session.secret)
}
});
const wait_and_then_create_an_event = new Promise((resolve) => {
setTimeout(async () => {
await client.fetch(`/rooms/${room.id}/events`, {
method: 'POST',
headers: {
'x-session_id': owner_info.session.id,
'x-totp': await generateTotp(owner_info.session.secret)
},
json: {
type: 'test',
data: {
i: 12345
}
}
});
resolve(undefined);
}, 2_000);
});
await Promise.all([long_poll_request_promise, wait_and_then_create_an_event]).then((values) => {
const long_polled_events = values.shift();
asserts.assert(Array.isArray(long_polled_events));
asserts.assertEquals(long_polled_events.length, 1);
asserts.assertEquals(long_polled_events[0].data?.i, 12345);
});
} finally {
clear_room_events_cache();
if (test_server_info) {
await test_server_info?.server?.stop();
}
}
}
});

View file

@ -2,6 +2,7 @@ import * as asserts from 'jsr:@std/assert';
import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../../helpers.ts'; import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../../helpers.ts';
import { api, API_CLIENT } from '../../../../utils/api.ts'; import { api, API_CLIENT } from '../../../../utils/api.ts';
import { generateTotp } from '@stdext/crypto/totp'; import { generateTotp } from '@stdext/crypto/totp';
import { clear_room_events_cache } from '../../../../models/event.ts';
Deno.test({ Deno.test({
name: 'API - ROOMS - EVENTS - Update', name: 'API - ROOMS - EVENTS - Update',
@ -235,6 +236,7 @@ Deno.test({
asserts.assertEquals(delete_owner_event_response.deleted, true); asserts.assertEquals(delete_owner_event_response.deleted, true);
} finally { } finally {
clear_room_events_cache();
if (test_server_info) { if (test_server_info) {
await test_server_info?.server?.stop(); await test_server_info?.server?.stop();
} }

View file

@ -2,6 +2,7 @@ import * as asserts from 'jsr:@std/assert';
import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../../helpers.ts'; import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../../helpers.ts';
import { api, API_CLIENT } from '../../../../utils/api.ts'; import { api, API_CLIENT } from '../../../../utils/api.ts';
import { generateTotp } from '@stdext/crypto/totp'; import { generateTotp } from '@stdext/crypto/totp';
import { clear_room_events_cache } from '../../../../models/event.ts';
Deno.test({ Deno.test({
name: 'API - ROOMS - EVENTS - Update (APPEND_ONLY_EVENTS)', name: 'API - ROOMS - EVENTS - Update (APPEND_ONLY_EVENTS)',
@ -159,6 +160,7 @@ Deno.test({
} finally { } finally {
Deno.env.delete('APPEND_ONLY_EVENTS'); Deno.env.delete('APPEND_ONLY_EVENTS');
clear_room_events_cache();
if (test_server_info) { if (test_server_info) {
await test_server_info?.server?.stop(); await test_server_info?.server?.stop();
} }

View file

@ -2,6 +2,7 @@ import { api, API_CLIENT } from '../../../utils/api.ts';
import * as asserts from 'jsr:@std/assert'; import * as asserts from 'jsr:@std/assert';
import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../helpers.ts'; import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../helpers.ts';
import { generateTotp } from '@stdext/crypto/totp'; import { generateTotp } from '@stdext/crypto/totp';
import { clear_room_events_cache } from '../../../models/event.ts';
Deno.test({ Deno.test({
name: 'API - ROOMS - Update', name: 'API - ROOMS - Update',
@ -91,6 +92,7 @@ Deno.test({
asserts.assertEquals(updated_by_other_user_room.topic, 'this is a newer topic'); asserts.assertEquals(updated_by_other_user_room.topic, 'this is a newer topic');
asserts.assertEquals(updated_by_other_user_room.permissions.write, [user_info.user.id, other_user_info.user.id]); asserts.assertEquals(updated_by_other_user_room.permissions.write, [user_info.user.id, other_user_info.user.id]);
} finally { } finally {
clear_room_events_cache();
if (test_server_info) { if (test_server_info) {
await test_server_info?.server?.stop(); await test_server_info?.server?.stop();
} }