@xtr-dev/rondevu-server 0.5.20 → 0.5.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/docker-publish.yml +58 -0
- package/Dockerfile +21 -10
- package/build.js +10 -6
- package/package.json +1 -1
- package/src/rpc.ts +27 -1
- package/src/storage/d1.ts +22 -0
- package/src/storage/memory.ts +26 -0
- package/src/storage/mysql.ts +22 -0
- package/src/storage/postgres.ts +22 -0
- package/src/storage/sqlite.ts +24 -0
- package/src/storage/types.ts +7 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: Build and Publish Docker Image
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags: ['v*']
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [main]
|
|
9
|
+
|
|
10
|
+
env:
|
|
11
|
+
REGISTRY: ghcr.io
|
|
12
|
+
IMAGE_NAME: ${{ github.repository }}
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
build-and-push:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
packages: write
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout repository
|
|
23
|
+
uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- name: Set up Docker Buildx
|
|
26
|
+
uses: docker/setup-buildx-action@v3
|
|
27
|
+
|
|
28
|
+
- name: Log in to Container Registry
|
|
29
|
+
if: github.event_name != 'pull_request'
|
|
30
|
+
uses: docker/login-action@v3
|
|
31
|
+
with:
|
|
32
|
+
registry: ${{ env.REGISTRY }}
|
|
33
|
+
username: ${{ github.actor }}
|
|
34
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
35
|
+
|
|
36
|
+
- name: Extract metadata for Docker
|
|
37
|
+
id: meta
|
|
38
|
+
uses: docker/metadata-action@v5
|
|
39
|
+
with:
|
|
40
|
+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
41
|
+
tags: |
|
|
42
|
+
type=ref,event=branch
|
|
43
|
+
type=ref,event=pr
|
|
44
|
+
type=semver,pattern={{version}}
|
|
45
|
+
type=semver,pattern={{major}}.{{minor}}
|
|
46
|
+
type=sha,prefix=
|
|
47
|
+
|
|
48
|
+
- name: Build and push Docker image
|
|
49
|
+
uses: docker/build-push-action@v5
|
|
50
|
+
with:
|
|
51
|
+
context: .
|
|
52
|
+
push: ${{ github.event_name != 'pull_request' }}
|
|
53
|
+
tags: ${{ steps.meta.outputs.tags }}
|
|
54
|
+
labels: ${{ steps.meta.outputs.labels }}
|
|
55
|
+
build-args: |
|
|
56
|
+
VERSION=${{ github.sha }}
|
|
57
|
+
cache-from: type=gha
|
|
58
|
+
cache-to: type=gha,mode=max
|
package/Dockerfile
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
# Build stage
|
|
2
2
|
FROM node:20-alpine AS builder
|
|
3
3
|
|
|
4
|
+
# Version is passed as build arg (from git commit hash)
|
|
5
|
+
ARG VERSION=unknown
|
|
6
|
+
|
|
7
|
+
# Install build tools for better-sqlite3
|
|
8
|
+
RUN apk add --no-cache python3 make g++
|
|
9
|
+
|
|
4
10
|
WORKDIR /app
|
|
5
11
|
|
|
6
12
|
# Copy package files
|
|
7
13
|
COPY package*.json ./
|
|
8
14
|
|
|
9
|
-
# Install dependencies
|
|
15
|
+
# Install dependencies (including native modules)
|
|
10
16
|
RUN npm ci
|
|
11
17
|
|
|
12
18
|
# Copy source files
|
|
@@ -14,22 +20,30 @@ COPY tsconfig.json ./
|
|
|
14
20
|
COPY build.js ./
|
|
15
21
|
COPY src ./src
|
|
16
22
|
|
|
17
|
-
# Build TypeScript
|
|
18
|
-
RUN npm run build
|
|
23
|
+
# Build TypeScript with version embedded
|
|
24
|
+
RUN VERSION=$VERSION npm run build
|
|
19
25
|
|
|
20
26
|
# Production stage
|
|
21
27
|
FROM node:20-alpine
|
|
22
28
|
|
|
29
|
+
# Install build tools for better-sqlite3 native module
|
|
30
|
+
RUN apk add --no-cache python3 make g++
|
|
31
|
+
|
|
23
32
|
WORKDIR /app
|
|
24
33
|
|
|
25
|
-
#
|
|
34
|
+
# Copy package files and install production deps
|
|
26
35
|
COPY package*.json ./
|
|
27
36
|
RUN npm ci --omit=dev && \
|
|
28
|
-
npm
|
|
37
|
+
npm rebuild better-sqlite3 && \
|
|
38
|
+
npm cache clean --force && \
|
|
39
|
+
apk del python3 make g++
|
|
29
40
|
|
|
30
41
|
# Copy built files from builder
|
|
31
42
|
COPY --from=builder /app/dist ./dist
|
|
32
43
|
|
|
44
|
+
# Copy migrations for schema setup
|
|
45
|
+
COPY migrations ./migrations
|
|
46
|
+
|
|
33
47
|
# Create data directory for SQLite
|
|
34
48
|
RUN mkdir -p /app/data && \
|
|
35
49
|
chown -R node:node /app
|
|
@@ -39,12 +53,9 @@ USER node
|
|
|
39
53
|
|
|
40
54
|
# Environment variables with defaults
|
|
41
55
|
ENV PORT=3000
|
|
42
|
-
ENV STORAGE_TYPE=
|
|
43
|
-
ENV STORAGE_PATH=/app/data/sessions.db
|
|
44
|
-
ENV SESSION_TIMEOUT=300000
|
|
45
|
-
ENV CODE_CHARS=0123456789
|
|
46
|
-
ENV CODE_LENGTH=9
|
|
56
|
+
ENV STORAGE_TYPE=memory
|
|
47
57
|
ENV CORS_ORIGINS=*
|
|
58
|
+
ENV NODE_ENV=production
|
|
48
59
|
|
|
49
60
|
# Expose port
|
|
50
61
|
EXPOSE 3000
|
package/build.js
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
const esbuild = require('esbuild');
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
|
|
5
|
-
// Use
|
|
6
|
-
let version =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
// Use VERSION env var first (for Docker builds), then fall back to git commit hash
|
|
6
|
+
let version = process.env.VERSION;
|
|
7
|
+
if (!version || version === 'unknown') {
|
|
8
|
+
try {
|
|
9
|
+
version = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim();
|
|
10
|
+
} catch (e) {
|
|
11
|
+
console.warn('Could not get git commit hash, using "unknown"');
|
|
12
|
+
version = 'unknown';
|
|
13
|
+
}
|
|
11
14
|
}
|
|
15
|
+
console.log(`Building with version: ${version}`);
|
|
12
16
|
|
|
13
17
|
esbuild.build({
|
|
14
18
|
entryPoints: ['src/index.ts'],
|
package/package.json
CHANGED
package/src/rpc.ts
CHANGED
|
@@ -128,6 +128,10 @@ export interface DiscoverParams {
|
|
|
128
128
|
offset?: number;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
export interface CountOffersByTagsParams {
|
|
132
|
+
tags: string[];
|
|
133
|
+
}
|
|
134
|
+
|
|
131
135
|
export interface PublishOfferParams {
|
|
132
136
|
tags: string[];
|
|
133
137
|
offers: Array<{ sdp: string }>;
|
|
@@ -300,6 +304,28 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
300
304
|
};
|
|
301
305
|
},
|
|
302
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Count available offers by tags
|
|
309
|
+
*/
|
|
310
|
+
async countOffersByTags(params: CountOffersByTagsParams, publicKey, timestamp, signature, storage, config, request: RpcRequest) {
|
|
311
|
+
const { tags } = params;
|
|
312
|
+
|
|
313
|
+
const tagsValidation = validateTags(tags);
|
|
314
|
+
if (!tagsValidation.valid) {
|
|
315
|
+
throw new RpcError(ErrorCodes.INVALID_TAG, tagsValidation.error || 'Invalid tags');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const counts = await storage.countOffersByTags(tags);
|
|
319
|
+
|
|
320
|
+
// Convert Map to object for JSON serialization
|
|
321
|
+
const result: Record<string, number> = {};
|
|
322
|
+
for (const [tag, count] of counts) {
|
|
323
|
+
result[tag] = count;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return { counts: result };
|
|
327
|
+
},
|
|
328
|
+
|
|
303
329
|
/**
|
|
304
330
|
* Publish offers with tags
|
|
305
331
|
*/
|
|
@@ -669,7 +695,7 @@ const handlers: Record<string, RpcHandler> = {
|
|
|
669
695
|
};
|
|
670
696
|
|
|
671
697
|
// Methods that don't require authentication
|
|
672
|
-
const UNAUTHENTICATED_METHODS = new Set(['discover']);
|
|
698
|
+
const UNAUTHENTICATED_METHODS = new Set(['discover', 'countOffersByTags']);
|
|
673
699
|
|
|
674
700
|
/**
|
|
675
701
|
* Handle RPC batch request with header-based authentication
|
package/src/storage/d1.ts
CHANGED
|
@@ -508,6 +508,28 @@ export class D1Storage implements Storage {
|
|
|
508
508
|
return result?.count ?? 0;
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
+
async countOffersByTags(tags: string[]): Promise<Map<string, number>> {
|
|
512
|
+
const result = new Map<string, number>();
|
|
513
|
+
if (tags.length === 0) return result;
|
|
514
|
+
|
|
515
|
+
const now = Date.now();
|
|
516
|
+
|
|
517
|
+
// Query each tag individually using json_each
|
|
518
|
+
for (const tag of tags) {
|
|
519
|
+
const queryResult = await this.db.prepare(`
|
|
520
|
+
SELECT COUNT(DISTINCT o.id) as count
|
|
521
|
+
FROM offers o, json_each(o.tags) as t
|
|
522
|
+
WHERE t.value = ?
|
|
523
|
+
AND o.expires_at > ?
|
|
524
|
+
AND o.answerer_public_key IS NULL
|
|
525
|
+
`).bind(tag, now).first() as { count: number } | null;
|
|
526
|
+
|
|
527
|
+
result.set(tag, queryResult?.count ?? 0);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
532
|
+
|
|
511
533
|
// ===== Helper Methods =====
|
|
512
534
|
|
|
513
535
|
private rowToOffer(row: any): Offer {
|
package/src/storage/memory.ts
CHANGED
|
@@ -445,6 +445,32 @@ export class MemoryStorage implements Storage {
|
|
|
445
445
|
return candidates ? candidates.length : 0;
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
+
async countOffersByTags(tags: string[]): Promise<Map<string, number>> {
|
|
449
|
+
const result = new Map<string, number>();
|
|
450
|
+
if (tags.length === 0) return result;
|
|
451
|
+
|
|
452
|
+
const now = Date.now();
|
|
453
|
+
|
|
454
|
+
for (const tag of tags) {
|
|
455
|
+
const offerIds = this.offersByTag.get(tag);
|
|
456
|
+
if (!offerIds) {
|
|
457
|
+
result.set(tag, 0);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
let count = 0;
|
|
462
|
+
for (const offerId of offerIds) {
|
|
463
|
+
const offer = this.offers.get(offerId);
|
|
464
|
+
if (offer && offer.expiresAt > now && !offer.answererPublicKey) {
|
|
465
|
+
count++;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
result.set(tag, count);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return result;
|
|
472
|
+
}
|
|
473
|
+
|
|
448
474
|
// ===== Helper Methods =====
|
|
449
475
|
|
|
450
476
|
private removeOfferFromIndexes(offer: Offer): void {
|
package/src/storage/mysql.ts
CHANGED
|
@@ -476,6 +476,28 @@ export class MySQLStorage implements Storage {
|
|
|
476
476
|
return Number(rows[0].count);
|
|
477
477
|
}
|
|
478
478
|
|
|
479
|
+
async countOffersByTags(tags: string[]): Promise<Map<string, number>> {
|
|
480
|
+
const result = new Map<string, number>();
|
|
481
|
+
if (tags.length === 0) return result;
|
|
482
|
+
|
|
483
|
+
const now = Date.now();
|
|
484
|
+
|
|
485
|
+
// Query each tag individually using JSON_CONTAINS
|
|
486
|
+
for (const tag of tags) {
|
|
487
|
+
const [rows] = await this.pool.query<RowDataPacket[]>(
|
|
488
|
+
`SELECT COUNT(DISTINCT id) as count
|
|
489
|
+
FROM offers
|
|
490
|
+
WHERE JSON_CONTAINS(tags, ?)
|
|
491
|
+
AND expires_at > ?
|
|
492
|
+
AND answerer_public_key IS NULL`,
|
|
493
|
+
[JSON.stringify(tag), now]
|
|
494
|
+
);
|
|
495
|
+
result.set(tag, Number(rows[0].count));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return result;
|
|
499
|
+
}
|
|
500
|
+
|
|
479
501
|
// ===== Helper Methods =====
|
|
480
502
|
|
|
481
503
|
private rowToOffer(row: RowDataPacket): Offer {
|
package/src/storage/postgres.ts
CHANGED
|
@@ -484,6 +484,28 @@ export class PostgreSQLStorage implements Storage {
|
|
|
484
484
|
return Number(result.rows[0].count);
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
async countOffersByTags(tags: string[]): Promise<Map<string, number>> {
|
|
488
|
+
const result = new Map<string, number>();
|
|
489
|
+
if (tags.length === 0) return result;
|
|
490
|
+
|
|
491
|
+
const now = Date.now();
|
|
492
|
+
|
|
493
|
+
// Query each tag individually using JSONB containment
|
|
494
|
+
for (const tag of tags) {
|
|
495
|
+
const queryResult = await this.pool.query(
|
|
496
|
+
`SELECT COUNT(DISTINCT id) as count
|
|
497
|
+
FROM offers
|
|
498
|
+
WHERE tags ? $1
|
|
499
|
+
AND expires_at > $2
|
|
500
|
+
AND answerer_public_key IS NULL`,
|
|
501
|
+
[tag, now]
|
|
502
|
+
);
|
|
503
|
+
result.set(tag, Number(queryResult.rows[0].count));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return result;
|
|
507
|
+
}
|
|
508
|
+
|
|
487
509
|
// ===== Helper Methods =====
|
|
488
510
|
|
|
489
511
|
private rowToOffer(row: any): Offer {
|
package/src/storage/sqlite.ts
CHANGED
|
@@ -543,6 +543,30 @@ export class SQLiteStorage implements Storage {
|
|
|
543
543
|
return result.count;
|
|
544
544
|
}
|
|
545
545
|
|
|
546
|
+
async countOffersByTags(tags: string[]): Promise<Map<string, number>> {
|
|
547
|
+
const result = new Map<string, number>();
|
|
548
|
+
if (tags.length === 0) return result;
|
|
549
|
+
|
|
550
|
+
const now = Date.now();
|
|
551
|
+
|
|
552
|
+
// Query counts for each tag individually for accuracy
|
|
553
|
+
// (an offer with multiple matching tags should only count once per tag)
|
|
554
|
+
const stmt = this.db.prepare(`
|
|
555
|
+
SELECT COUNT(DISTINCT o.id) as count
|
|
556
|
+
FROM offers o, json_each(o.tags) as t
|
|
557
|
+
WHERE t.value = ?
|
|
558
|
+
AND o.expires_at > ?
|
|
559
|
+
AND o.answerer_public_key IS NULL
|
|
560
|
+
`);
|
|
561
|
+
|
|
562
|
+
for (const tag of tags) {
|
|
563
|
+
const row = stmt.get(tag, now) as { count: number };
|
|
564
|
+
result.set(tag, row.count);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return result;
|
|
568
|
+
}
|
|
569
|
+
|
|
546
570
|
// ===== Helper Methods =====
|
|
547
571
|
|
|
548
572
|
/**
|
package/src/storage/types.ts
CHANGED
|
@@ -255,4 +255,11 @@ export interface Storage {
|
|
|
255
255
|
* @returns ICE candidate count for offer
|
|
256
256
|
*/
|
|
257
257
|
getIceCandidateCount(offerId: string): Promise<number>;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Counts available (unanswered, non-expired) offers for each of the given tags
|
|
261
|
+
* @param tags Array of tags to count offers for
|
|
262
|
+
* @returns Map of tag to offer count
|
|
263
|
+
*/
|
|
264
|
+
countOffersByTags(tags: string[]): Promise<Map<string, number>>;
|
|
258
265
|
}
|