Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ddcda122a |
@@ -28,7 +28,10 @@ import {
|
||||
} from 'matrix-js-sdk';
|
||||
import { getCallCapabilities } from './utils';
|
||||
import { downloadMedia, mxcUrlToHttp } from '../../utils/matrix';
|
||||
import { dedupeInFlight } from '../../utils/inFlight';
|
||||
|
||||
/** A little longer than Element Call's own 2 s timeout for a restart. */
|
||||
const RESTART_TIMEOUT_MS = 5000;
|
||||
export class CallWidgetDriver extends WidgetDriver {
|
||||
private allowedCapabilities: Set<Capability>;
|
||||
|
||||
@@ -129,8 +132,18 @@ export class CallWidgetDriver extends WidgetDriver {
|
||||
await this.mx._unstable_cancelScheduledDelayedEvent(delayId);
|
||||
}
|
||||
|
||||
// [Gitea #318] While the homeserver is stalled, Element Call times out each
|
||||
// restart locally after 2 s and asks again, so requests for the same delay
|
||||
// id piled up and hit the server together on recovery (404s, then 429s).
|
||||
// Share one in-flight restart per id, and give it a timeout of its own.
|
||||
private readonly restartOnce = dedupeInFlight<unknown>();
|
||||
|
||||
public async restartScheduledDelayedEvent(delayId: string): Promise<void> {
|
||||
await this.mx._unstable_restartScheduledDelayedEvent(delayId);
|
||||
await this.restartOnce(delayId, () =>
|
||||
this.mx._unstable_restartScheduledDelayedEvent(delayId, {
|
||||
localTimeoutMs: RESTART_TIMEOUT_MS,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async sendScheduledDelayedEvent(delayId: string): Promise<void> {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { dedupeInFlight } from './inFlight';
|
||||
|
||||
test('dedupeInFlight: one request per key while pending, a new one after (#318)', async () => {
|
||||
const once = dedupeInFlight<number>();
|
||||
let calls = 0;
|
||||
let release!: () => void;
|
||||
const gate = new Promise<void>((r) => {
|
||||
release = r;
|
||||
});
|
||||
const run = async () => {
|
||||
calls += 1;
|
||||
await gate;
|
||||
return calls;
|
||||
};
|
||||
const a = once('d1', run);
|
||||
const b = once('d1', run);
|
||||
const c = once('d2', run);
|
||||
assert.equal(a, b);
|
||||
release();
|
||||
assert.deepEqual(await Promise.all([a, b, c]), [2, 2, 2]);
|
||||
assert.equal(calls, 2);
|
||||
await once('d1', run);
|
||||
assert.equal(calls, 3, 'settled requests are not reused');
|
||||
});
|
||||
|
||||
test('dedupeInFlight: a rejection clears the key', async () => {
|
||||
const once = dedupeInFlight<void>();
|
||||
await assert.rejects(once('k', () => Promise.reject(new Error('boom'))));
|
||||
await once('k', () => Promise.resolve());
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Share one in-flight promise per key: callers that ask again while a request
|
||||
* for the same key is still pending get that request instead of a new one.
|
||||
*/
|
||||
export function dedupeInFlight<T>(): (key: string, run: () => Promise<T>) => Promise<T> {
|
||||
const pending = new Map<string, Promise<T>>();
|
||||
return (key, run) => {
|
||||
const existing = pending.get(key);
|
||||
if (existing) return existing;
|
||||
const p = run().finally(() => pending.delete(key));
|
||||
pending.set(key, p);
|
||||
return p;
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
plain,
|
||||
DEFAULT_GROUPS,
|
||||
kumaBeatTime,
|
||||
kumaUrls,
|
||||
@@ -261,27 +260,3 @@ test('a dismissed calls-down banner lets the next one through, never hides serve
|
||||
'server-down',
|
||||
);
|
||||
});
|
||||
|
||||
test('plain: strips Markdown syntax only, keeps ordinary #, _, > and * (#308)', () => {
|
||||
assert.equal(
|
||||
plain('join #lotus-general or #announcements:lotusguild.org'),
|
||||
'join #lotus-general or #announcements:lotusguild.org',
|
||||
);
|
||||
assert.equal(
|
||||
plain('ping @some_user about snake_case and C#'),
|
||||
'ping @some_user about snake_case and C#',
|
||||
);
|
||||
assert.equal(
|
||||
plain('calls -> fixed, > 5 people, 5 * 3 = 15'),
|
||||
'calls -> fixed, > 5 people, 5 * 3 = 15',
|
||||
);
|
||||
assert.equal(
|
||||
plain('# Heading\n* item one\n- item two\n> quoted'),
|
||||
'Heading\nitem one\nitem two\nquoted',
|
||||
);
|
||||
assert.equal(plain('**bold**, *em*, _em_, `/sfu/get`'), 'bold, em, em, /sfu/get');
|
||||
assert.equal(
|
||||
plain('Read the <b>guide</b> <img src=x onerror=alert(1)> [docs](https://x.y)'),
|
||||
'Read the guide docs',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -240,25 +240,12 @@ const incidentTone = (style: string): BannerTone => {
|
||||
};
|
||||
|
||||
/** Markdown-ish incident text as plain text for the Details line. */
|
||||
/**
|
||||
* [Gitea #308] Markdown → plain text for the Details line. Strips only the
|
||||
* syntax: removing every `*_\`#>` also broke "#lotus-general", "snake_case",
|
||||
* "->" and "5 * 3". HTML tags are dropped (React escapes the text anyway).
|
||||
*/
|
||||
export const plain = (s: string): string =>
|
||||
const plain = (s: string): string =>
|
||||
s
|
||||
.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
|
||||
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
||||
.replace(/<\/?[a-z][^>]*>/gi, '')
|
||||
.replace(/^[ \t]{0,3}#{1,6}[ \t]+/gm, '')
|
||||
.replace(/^[ \t]*>[ \t]?/gm, '')
|
||||
.replace(/^[ \t]*[*+-][ \t]+/gm, '')
|
||||
.replace(/`([^`\n]+)`/g, '$1')
|
||||
.replace(/\*\*(?=\S)([^*\n]*?\S)\*\*/g, '$1')
|
||||
.replace(/__(?=\S)([^_\n]*?\S)__/g, '$1')
|
||||
.replace(/(^|[^\w*])\*(?=\S)([^*\n]*?\S)\*(?![\w*])/g, '$1$2')
|
||||
.replace(/(^|[^\w])_(?=\S)([^_\n]*?\S)_(?!\w)/g, '$1$2')
|
||||
.replace(/[ \t]+\n/g, '\n')
|
||||
.replace(/[*_`#>]/g, '')
|
||||
.replace(/\s+\n/g, '\n')
|
||||
.trim();
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user