From b7002512786d4cd66072b3565b43224efc25a73b Mon Sep 17 00:00:00 2001 From: Andy Burke Date: Wed, 2 Jul 2025 21:28:07 -0700 Subject: [PATCH] feature: events polling --- .zed/settings.json | 21 +++ deno.json | 6 +- deno.lock | 63 +++++++-- models/event.ts | 75 +++++++---- public/api/auth/index.ts | 2 +- .../rooms/:room_id/events/:event_id/index.ts | 16 ++- public/api/rooms/:room_id/events/index.ts | 93 +++++++++++--- public/api/users/:user_id/index.ts | 6 +- public/base.css | 11 +- public/signup_login_wall.html | 1 - public/tabs/tabs.html | 2 +- public/tabs/talk.html | 1 + tests/api/rooms/create_room.test.ts | 2 + tests/api/rooms/delete_room.test.ts | 2 + tests/api/rooms/events/create_events.test.ts | 2 + tests/api/rooms/events/get_events.test.ts | 121 ++++++++++++++++++ tests/api/rooms/events/update_events.test.ts | 2 + .../update_events_when_append_only.test.ts | 2 + tests/api/rooms/update_room.test.ts | 2 + 19 files changed, 353 insertions(+), 77 deletions(-) create mode 100644 .zed/settings.json create mode 100644 tests/api/rooms/events/get_events.test.ts diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..adf444e --- /dev/null +++ b/.zed/settings.json @@ -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" + } + } +} diff --git a/deno.json b/deno.json index 6f8b356..fb067d3 100644 --- a/deno.json +++ b/deno.json @@ -32,13 +32,13 @@ } }, "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/serverus": "jsr:@andyburke/serverus@^0.7.1", "@std/assert": "jsr:@std/assert@^1.0.13", "@std/encoding": "jsr:@std/encoding@^1.0.10", - "@std/http": "jsr:@std/http@^1.0.18", - "@std/path": "jsr:@std/path@^1.1.0", + "@std/http": "jsr:@std/http@^1.0.19", + "@std/path": "jsr:@std/path@^1.1.1", "@stdext/crypto": "jsr:@stdext/crypto@^0.1.0" } } diff --git a/deno.lock b/deno.lock index 2183d6a..26b9edc 100644 --- a/deno.lock +++ b/deno.lock @@ -1,9 +1,11 @@ { "version": "5", "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.2.0", + "jsr:@andyburke/serverus@*": "0.7.1", "jsr:@andyburke/serverus@~0.7.1": "0.7.1", "jsr:@std/assert@*": "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/fmt@^1.0.6": "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.18": "1.0.18", + "jsr:@std/fs@^1.0.14": "1.0.19", + "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/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/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/net@^1.0.4": "1.0.4", - "jsr:@std/path@^1.0.8": "1.1.0", - "jsr:@std/path@^1.1.0": "1.1.0", + "jsr:@std/path@*": "1.1.1", + "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:@stdext/crypto@*": "0.1.0", "jsr:@stdext/crypto@0.1": "0.1.0" }, "jsr": { - "@andyburke/fsdb@0.6.1": { - "integrity": "059ad6702e40a39a188e648a8ebf2547087782becae040af916aa843830328ea", + "@andyburke/fsdb@0.9.0": { + "integrity": "726c138ac8b751c969cb045d9d1fc3541a054130aa741d256864b376afdb8f89", "dependencies": [ "jsr:@std/cli@^1.0.20", "jsr:@std/fs@^1.0.18", @@ -60,7 +67,7 @@ "@std/assert@1.0.13": { "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29", "dependencies": [ - "jsr:@std/internal" + "jsr:@std/internal@^1.0.6" ] }, "@std/async@1.0.13": { @@ -81,6 +88,13 @@ "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": { "integrity": "eff3497c08164e6ada49b7f81a28b5108087033823153d065e3f89467dd3d50e" }, @@ -101,9 +115,26 @@ "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": { "integrity": "fc66e846d8d38a47cffd274d80d2ca3f0de71040f855783724bb6b87f60891f5" }, + "@std/internal@1.0.9": { + "integrity": "bdfb97f83e4db7a13e8faab26fb1958d1b80cc64366501af78a0aee151696eb8" + }, "@std/media-types@1.1.0": { "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" }, @@ -113,6 +144,12 @@ "@std/path@1.1.0": { "integrity": "ddc94f8e3c275627281cbc23341df6b8bcc874d70374f75fec2533521e3d6886" }, + "@std/path@1.1.1": { + "integrity": "fe00026bd3a7e6a27f73709b83c607798be40e20c81dde655ce34052fd82ec76", + "dependencies": [ + "jsr:@std/internal@^1.0.9" + ] + }, "@std/streams@1.0.10": { "integrity": "75c0b1431873cd0d8b3d679015220204d36d3c7420d93b60acfc379eb0dc30af" }, @@ -165,13 +202,13 @@ }, "workspace": { "dependencies": [ - "jsr:@andyburke/fsdb@~0.6.1", + "jsr:@andyburke/fsdb@0.9", "jsr:@andyburke/lurid@0.2", "jsr:@andyburke/serverus@~0.7.1", "jsr:@std/assert@^1.0.13", "jsr:@std/encoding@^1.0.10", - "jsr:@std/http@^1.0.18", - "jsr:@std/path@^1.1.0", + "jsr:@std/http@^1.0.19", + "jsr:@std/path@^1.1.1", "jsr:@stdext/crypto@0.1" ] } diff --git a/models/event.ts b/models/event.ts index 5541ff3..18a8556 100644 --- a/models/event.ts +++ b/models/event.ts @@ -11,7 +11,7 @@ import { FSDB_INDEXER_SYMLINKS } from 'jsr:@andyburke/fsdb/indexers'; /** * 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} room_id - id of the target room * @property {string} type - event type @@ -31,28 +31,55 @@ export type EVENT = { }; }; -export const EVENTS = new FSDB_COLLECTION({ - name: 'events', - id_field: 'id', - 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({ - name: 'creator_id', - field: 'creator_id', - to_many: true, - organize: by_lurid - }), +type ROOM_EVENT_CACHE_ENTRY = { + collection: FSDB_COLLECTION; + eviction_timeout: number; +}; - tags: new FSDB_INDEXER_SYMLINKS({ - name: 'tags', - get_values_to_index: (event: EVENT): string[] => { - return (event.tags ?? []).map((tag: string) => tag.toLowerCase()); - }, - to_many: true, - organize: by_character - }) +const ROOM_EVENTS: Record = {}; +export function get_events_collection_for_room(room_id: string): FSDB_COLLECTION { + ROOM_EVENTS[room_id] = ROOM_EVENTS[room_id] ?? { + collection: new FSDB_COLLECTION({ + name: `rooms/${room_id.substring(0, 14)}/${room_id.substring(0, 36)}/${room_id}/events`, + id_field: 'id', + organize: by_lurid, + indexers: { + creator_id: new FSDB_INDEXER_SYMLINKS({ + name: 'creator_id', + field: 'creator_id', + to_many: true, + organize: by_lurid + }), + + tags: new FSDB_INDEXER_SYMLINKS({ + 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]; + } +} diff --git a/public/api/auth/index.ts b/public/api/auth/index.ts index 3a8743d..583ef6d 100644 --- a/public/api/auth/index.ts +++ b/public/api/auth/index.ts @@ -53,7 +53,7 @@ export async function POST(req: Request, meta: Record): Promise): Promise { - const event: EVENT | null = await EVENTS.get(meta.params.event_id); + const events: FSDB_COLLECTION = get_events_collection_for_room(meta.room.id); + const event: EVENT | null = await events.get(meta.params.event_id); if (!event) { return CANNED_RESPONSES.not_found(); @@ -76,7 +78,8 @@ export async function PUT(req: Request, meta: Record): Promise = get_events_collection_for_room(meta.room.id); + const event: EVENT | null = await events.get(meta.params.event_id); if (!event) { return CANNED_RESPONSES.not_found(); @@ -98,7 +101,7 @@ export async function PUT(req: Request, meta: Record): Promise): Promise { - const event: EVENT | null = await EVENTS.get(meta.params.event_id); + const events: FSDB_COLLECTION = get_events_collection_for_room(meta.room.id); + const event: EVENT | null = await events.get(meta.params.event_id); if (!event) { return CANNED_RESPONSES.not_found(); } - await EVENTS.delete(event); + await events.delete(event); return Response.json({ deleted: true diff --git a/public/api/rooms/:room_id/events/index.ts b/public/api/rooms/:room_id/events/index.ts index 115823c..e7409a6 100644 --- a/public/api/rooms/:room_id/events/index.ts +++ b/public/api/rooms/:room_id/events/index.ts @@ -2,8 +2,10 @@ import lurid from 'jsr:@andyburke/lurid'; import { get_session, get_user, PRECHECK_TABLE, require_user } from '../../../../../utils/prechecks.ts'; import { ROOM, ROOMS } from '../../../../../models/room.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 { FSDB_COLLECTION, FSDB_SEARCH_OPTIONS, WALK_ENTRY } from 'jsr:@andyburke/fsdb'; +import * as path from 'jsr:@std/path'; 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(); } }]; -export async function GET(_req: Request, meta: Record): Promise { - const query: URLSearchParams = meta.query; - const partial_id: string | undefined = query.get('partial_id')?.toLowerCase().trim(); +export async function GET(request: Request, meta: Record): Promise { + const events: FSDB_COLLECTION = get_events_collection_for_room(meta.room.id); - const has_partial_id = typeof partial_id === 'string' && partial_id.length >= 2; - if (!has_partial_id) { + const sorts = events.sorts; + 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({ error: { - message: 'You must specify a `partial_id` query parameter.', - cause: 'missing_query_parameter' + message: 'You must specify a sort: newest, oldest, latest, stalest', + cause: 'invalid_sort' } }, { status: 400 }); } - const limit = Math.min(parseInt(query.get('limit') ?? '10'), 100); - const events = await EVENTS.all({ - id_after: partial_id, - limit - }); + const options: FSDB_SEARCH_OPTIONS = { + ...(meta.query ?? {}), + limit: Math.min(parseInt(meta.query?.limit ?? '10'), 1_000), + sort, + filter: (entry: WALK_ENTRY) => { + const event_id = path.basename(entry.path).replace(/\.json$/i, ''); - return Response.json(events, { - status: 200 + if (meta.query.after_id && event_id <= meta.query.after_id) { + 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): Promise { try { + const events: FSDB_COLLECTION = get_events_collection_for_room(meta.room.id); + const now = new Date().toISOString(); + const body = await parse_body(req); - const new_event: EVENT = { + const event: EVENT = { type: 'unknown', ...body, - id: `${meta.params.room_id}:${lurid()}`, + id: lurid(), creator_id: meta.user.id, timestamps: { created: now, @@ -95,9 +150,9 @@ export async function POST(req: Request, meta: Record): Promise): Promise< await PASSWORD_ENTRIES.delete(password_entry); } - const sessions = await SESSIONS.find({ + const session_entries = await SESSIONS.find({ user_id }); - for (const session of sessions) { - await SESSIONS.delete(session); + for (const entry of session_entries) { + await SESSIONS.delete(entry.load()); } await USERS.delete(user); diff --git a/public/base.css b/public/base.css index cd0a03e..33b89eb 100644 --- a/public/base.css +++ b/public/base.css @@ -1,8 +1,8 @@ /* Dark mode default */ :root { - --bg: #121212; - --text: #f0f0f0; - --accent: #4caf50; + --bg: #323232; + --text: #efe; + --accent: #fa0 ; --border-subtle: #555; --border-normal: #888; --border-highlight: #bbb; @@ -13,7 +13,7 @@ :root { --bg: #f0f0f0; --text: #121212; - --accent: #4caf50; + --accent: #c80; --border-subtle: #bbb; --border-normal: #888; --border-highlight: #555; @@ -135,9 +135,8 @@ button { background: inherit; color: inherit; padding: 0.5rem; - margin: 0 1rem; border: 1px solid var(--text); - border-radius: 10%; + border-radius: 4px; } button.primary { diff --git a/public/signup_login_wall.html b/public/signup_login_wall.html index c0b20d7..ee05a1a 100644 --- a/public/signup_login_wall.html +++ b/public/signup_login_wall.html @@ -150,7 +150,6 @@ document.body.dataset.user = JSON.stringify(response.user); document.body.dataset.perms = response.user.permissions.join(":"); - console.dir({ response }); }; } diff --git a/public/tabs/tabs.html b/public/tabs/tabs.html index 1af63c9..5c91597 100644 --- a/public/tabs/tabs.html +++ b/public/tabs/tabs.html @@ -90,7 +90,7 @@ .tab-switch:checked + .tab-label { margin-top: 1px; - border-bottom: 1px solid var(--border-highlight); + border-bottom: 1px solid var(--accent); z-index: 1; } diff --git a/public/tabs/talk.html b/public/tabs/talk.html index 486d93f..1acdb20 100644 --- a/public/tabs/talk.html +++ b/public/tabs/talk.html @@ -71,6 +71,7 @@ #talk #room-chat-entry-container form button { width: inherit; padding: inherit; + margin: 0 1rem; } #talk #room-chat-entry-container form textarea { diff --git a/tests/api/rooms/create_room.test.ts b/tests/api/rooms/create_room.test.ts index 7a4ac1b..199e6e5 100644 --- a/tests/api/rooms/create_room.test.ts +++ b/tests/api/rooms/create_room.test.ts @@ -2,6 +2,7 @@ import { api, API_CLIENT } from '../../../utils/api.ts'; import * as asserts from 'jsr:@std/assert'; import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../helpers.ts'; import { generateTotp } from '@stdext/crypto/totp'; +import { clear_room_events_cache } from '../../../models/event.ts'; Deno.test({ name: 'API - ROOMS - Create', @@ -72,6 +73,7 @@ Deno.test({ asserts.assert(new_room); } finally { + clear_room_events_cache(); if (test_server_info) { await test_server_info?.server?.stop(); } diff --git a/tests/api/rooms/delete_room.test.ts b/tests/api/rooms/delete_room.test.ts index e4a261a..05926a7 100644 --- a/tests/api/rooms/delete_room.test.ts +++ b/tests/api/rooms/delete_room.test.ts @@ -2,6 +2,7 @@ import { api, API_CLIENT } from '../../../utils/api.ts'; import * as asserts from 'jsr:@std/assert'; import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../helpers.ts'; import { generateTotp } from '@stdext/crypto/totp'; +import { clear_room_events_cache } from '../../../models/event.ts'; Deno.test({ name: 'API - ROOMS - Delete', @@ -48,6 +49,7 @@ Deno.test({ asserts.assert(deleted_room); } finally { + clear_room_events_cache(); if (test_server_info) { await test_server_info?.server?.stop(); } diff --git a/tests/api/rooms/events/create_events.test.ts b/tests/api/rooms/events/create_events.test.ts index 1561f46..ca6166a 100644 --- a/tests/api/rooms/events/create_events.test.ts +++ b/tests/api/rooms/events/create_events.test.ts @@ -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 { 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 - Create', @@ -113,6 +114,7 @@ Deno.test({ asserts.assert(event_from_other_user); } finally { + clear_room_events_cache(); if (test_server_info) { await test_server_info?.server?.stop(); } diff --git a/tests/api/rooms/events/get_events.test.ts b/tests/api/rooms/events/get_events.test.ts new file mode 100644 index 0000000..abd1d2d --- /dev/null +++ b/tests/api/rooms/events/get_events.test.ts @@ -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(); + } + } + } +}); diff --git a/tests/api/rooms/events/update_events.test.ts b/tests/api/rooms/events/update_events.test.ts index 1e3d55a..282a6c2 100644 --- a/tests/api/rooms/events/update_events.test.ts +++ b/tests/api/rooms/events/update_events.test.ts @@ -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 { 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 - Update', @@ -235,6 +236,7 @@ Deno.test({ asserts.assertEquals(delete_owner_event_response.deleted, true); } finally { + clear_room_events_cache(); if (test_server_info) { await test_server_info?.server?.stop(); } diff --git a/tests/api/rooms/events/update_events_when_append_only.test.ts b/tests/api/rooms/events/update_events_when_append_only.test.ts index d94f7af..8cf4725 100644 --- a/tests/api/rooms/events/update_events_when_append_only.test.ts +++ b/tests/api/rooms/events/update_events_when_append_only.test.ts @@ -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 { 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 - Update (APPEND_ONLY_EVENTS)', @@ -159,6 +160,7 @@ Deno.test({ } finally { Deno.env.delete('APPEND_ONLY_EVENTS'); + clear_room_events_cache(); if (test_server_info) { await test_server_info?.server?.stop(); } diff --git a/tests/api/rooms/update_room.test.ts b/tests/api/rooms/update_room.test.ts index 294ec18..cfe4194 100644 --- a/tests/api/rooms/update_room.test.ts +++ b/tests/api/rooms/update_room.test.ts @@ -2,6 +2,7 @@ import { api, API_CLIENT } from '../../../utils/api.ts'; import * as asserts from 'jsr:@std/assert'; import { EPHEMERAL_SERVER, get_ephemeral_listen_server, get_new_user, set_user_permissions } from '../../helpers.ts'; import { generateTotp } from '@stdext/crypto/totp'; +import { clear_room_events_cache } from '../../../models/event.ts'; Deno.test({ 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.permissions.write, [user_info.user.id, other_user_info.user.id]); } finally { + clear_room_events_cache(); if (test_server_info) { await test_server_info?.server?.stop(); }