diff --git a/categories.example.json b/categories.example.json index 04cf8e1..2cd3487 100644 --- a/categories.example.json +++ b/categories.example.json @@ -69,7 +69,16 @@ "mediaFormat": { "type": "string", "description": "Physical media format if applicable", - "options": ["CD", "DVD", "Vinyl", "Soundboard", "SACD", "DAT", "WEB", "Blu-Ray"] + "options": [ + "CD", + "DVD", + "Vinyl", + "Soundboard", + "SACD", + "DAT", + "WEB", + "Blu-Ray" + ] } } }, @@ -198,7 +207,14 @@ "status": { "type": "string", "description": "Current status of the TV show", - "options": ["Returning Series", "Ended", "Canceled", "In Production", "Pilot", "Unknown"] + "options": [ + "Returning Series", + "Ended", + "Canceled", + "In Production", + "Pilot", + "Unknown" + ] }, "TMDBID": { "type": "string", @@ -226,4 +242,4 @@ } } } -] \ No newline at end of file +] diff --git a/package.json b/package.json index a0b80e1..621144d 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,17 @@ { "name": "@riffcc/orbiter", - "version": "0.2.34", + "version": "0.2.34-dev9", "description": "", "main": "dist/index.js", "type": "module", + "exports": { + ".": "./dist/index.js", + "./orbitedb-hook": "./dist/orbitedb-hook.js", + "./constellation-patch": "./dist/constellation-patch.js", + "./utils/db-retry": "./dist/utils/db-retry.js", + "./lens-client": "./dist/lens-client.js", + "./lens-server": "./dist/lens-server.js" + }, "scripts": { "clean": "rimraf dist", "compile": "pnpm update-version && pnpm clean && pnpm tspc -p tsconfig.json", @@ -38,6 +46,7 @@ "@eslint/js": "^9.25.1", "@types/lodash-es": "^4.17.12", "@types/mocha": "^10.0.10", + "@types/node": "^22.15.18", "@types/yargs": "^17.0.33", "aegir": "^45.2.0", "ajv": "^8.17.1", @@ -63,11 +72,13 @@ "dependencies": { "@constl/utils-ipa": "2.0.2", "@inquirer/prompts": "^7.4.1", + "abstract-level": "^1.0.3", "chalk": "^5.4.1", "change-case": "^5.4.4", "constl-ipa-fork": "^1.8.67", "deepcopy": "^2.1.0", "dotenv": "^16.5.0", + "level": "^8.0.0", "lodash-es": "^4.17.21", "log-update": "^6.1.0", "ora": "^8.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 29fdd1a..6525ce6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,10 @@ importers: version: 2.0.2 '@inquirer/prompts': specifier: ^7.4.1 - version: 7.4.1(@types/node@22.14.1) + version: 7.4.1(@types/node@22.15.18) + abstract-level: + specifier: ^1.0.3 + version: 1.0.4 chalk: specifier: ^5.4.1 version: 5.4.1 @@ -29,6 +32,9 @@ importers: dotenv: specifier: ^16.5.0 version: 16.5.0 + level: + specifier: ^8.0.0 + version: 8.0.1 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -66,6 +72,9 @@ importers: '@types/mocha': specifier: ^10.0.10 version: 10.0.10 + '@types/node': + specifier: ^22.15.18 + version: 22.15.18 '@types/yargs': specifier: ^17.0.33 version: 17.0.33 @@ -113,7 +122,7 @@ importers: version: 3.0.0 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@22.14.1)(typescript@5.6.3) + version: 10.9.2(@types/node@22.15.18)(typescript@5.6.3) ts-patch: specifier: ^3.3.0 version: 3.3.0 @@ -2620,6 +2629,9 @@ packages: '@types/node@22.14.1': resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==} + '@types/node@22.15.18': + resolution: {integrity: sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==} + '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -10867,27 +10879,27 @@ snapshots: '@humanwhocodes/retry@0.4.2': {} - '@inquirer/checkbox@4.1.5(@types/node@22.14.1)': + '@inquirer/checkbox@4.1.5(@types/node@22.15.18)': dependencies: - '@inquirer/core': 10.1.10(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.15.18) '@inquirer/figures': 1.0.11 - '@inquirer/type': 3.0.6(@types/node@22.14.1) + '@inquirer/type': 3.0.6(@types/node@22.15.18) ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.18 - '@inquirer/confirm@5.1.9(@types/node@22.14.1)': + '@inquirer/confirm@5.1.9(@types/node@22.15.18)': dependencies: - '@inquirer/core': 10.1.10(@types/node@22.14.1) - '@inquirer/type': 3.0.6(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.15.18) + '@inquirer/type': 3.0.6(@types/node@22.15.18) optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.18 - '@inquirer/core@10.1.10(@types/node@22.14.1)': + '@inquirer/core@10.1.10(@types/node@22.15.18)': dependencies: '@inquirer/figures': 1.0.11 - '@inquirer/type': 3.0.6(@types/node@22.14.1) + '@inquirer/type': 3.0.6(@types/node@22.15.18) ansi-escapes: 4.3.2 cli-width: 4.1.0 mute-stream: 2.0.0 @@ -10895,93 +10907,93 @@ snapshots: wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.18 - '@inquirer/editor@4.2.10(@types/node@22.14.1)': + '@inquirer/editor@4.2.10(@types/node@22.15.18)': dependencies: - '@inquirer/core': 10.1.10(@types/node@22.14.1) - '@inquirer/type': 3.0.6(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.15.18) + '@inquirer/type': 3.0.6(@types/node@22.15.18) external-editor: 3.1.0 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.18 - '@inquirer/expand@4.0.12(@types/node@22.14.1)': + '@inquirer/expand@4.0.12(@types/node@22.15.18)': dependencies: - '@inquirer/core': 10.1.10(@types/node@22.14.1) - '@inquirer/type': 3.0.6(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.15.18) + '@inquirer/type': 3.0.6(@types/node@22.15.18) yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.18 '@inquirer/figures@1.0.11': {} - '@inquirer/input@4.1.9(@types/node@22.14.1)': + '@inquirer/input@4.1.9(@types/node@22.15.18)': dependencies: - '@inquirer/core': 10.1.10(@types/node@22.14.1) - '@inquirer/type': 3.0.6(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.15.18) + '@inquirer/type': 3.0.6(@types/node@22.15.18) optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.18 - '@inquirer/number@3.0.12(@types/node@22.14.1)': + '@inquirer/number@3.0.12(@types/node@22.15.18)': dependencies: - '@inquirer/core': 10.1.10(@types/node@22.14.1) - '@inquirer/type': 3.0.6(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.15.18) + '@inquirer/type': 3.0.6(@types/node@22.15.18) optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.18 - '@inquirer/password@4.0.12(@types/node@22.14.1)': + '@inquirer/password@4.0.12(@types/node@22.15.18)': dependencies: - '@inquirer/core': 10.1.10(@types/node@22.14.1) - '@inquirer/type': 3.0.6(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.15.18) + '@inquirer/type': 3.0.6(@types/node@22.15.18) ansi-escapes: 4.3.2 optionalDependencies: - '@types/node': 22.14.1 - - '@inquirer/prompts@7.4.1(@types/node@22.14.1)': - dependencies: - '@inquirer/checkbox': 4.1.5(@types/node@22.14.1) - '@inquirer/confirm': 5.1.9(@types/node@22.14.1) - '@inquirer/editor': 4.2.10(@types/node@22.14.1) - '@inquirer/expand': 4.0.12(@types/node@22.14.1) - '@inquirer/input': 4.1.9(@types/node@22.14.1) - '@inquirer/number': 3.0.12(@types/node@22.14.1) - '@inquirer/password': 4.0.12(@types/node@22.14.1) - '@inquirer/rawlist': 4.0.12(@types/node@22.14.1) - '@inquirer/search': 3.0.12(@types/node@22.14.1) - '@inquirer/select': 4.1.1(@types/node@22.14.1) + '@types/node': 22.15.18 + + '@inquirer/prompts@7.4.1(@types/node@22.15.18)': + dependencies: + '@inquirer/checkbox': 4.1.5(@types/node@22.15.18) + '@inquirer/confirm': 5.1.9(@types/node@22.15.18) + '@inquirer/editor': 4.2.10(@types/node@22.15.18) + '@inquirer/expand': 4.0.12(@types/node@22.15.18) + '@inquirer/input': 4.1.9(@types/node@22.15.18) + '@inquirer/number': 3.0.12(@types/node@22.15.18) + '@inquirer/password': 4.0.12(@types/node@22.15.18) + '@inquirer/rawlist': 4.0.12(@types/node@22.15.18) + '@inquirer/search': 3.0.12(@types/node@22.15.18) + '@inquirer/select': 4.1.1(@types/node@22.15.18) optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.18 - '@inquirer/rawlist@4.0.12(@types/node@22.14.1)': + '@inquirer/rawlist@4.0.12(@types/node@22.15.18)': dependencies: - '@inquirer/core': 10.1.10(@types/node@22.14.1) - '@inquirer/type': 3.0.6(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.15.18) + '@inquirer/type': 3.0.6(@types/node@22.15.18) yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.18 - '@inquirer/search@3.0.12(@types/node@22.14.1)': + '@inquirer/search@3.0.12(@types/node@22.15.18)': dependencies: - '@inquirer/core': 10.1.10(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.15.18) '@inquirer/figures': 1.0.11 - '@inquirer/type': 3.0.6(@types/node@22.14.1) + '@inquirer/type': 3.0.6(@types/node@22.15.18) yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.18 - '@inquirer/select@4.1.1(@types/node@22.14.1)': + '@inquirer/select@4.1.1(@types/node@22.15.18)': dependencies: - '@inquirer/core': 10.1.10(@types/node@22.14.1) + '@inquirer/core': 10.1.10(@types/node@22.15.18) '@inquirer/figures': 1.0.11 - '@inquirer/type': 3.0.6(@types/node@22.14.1) + '@inquirer/type': 3.0.6(@types/node@22.15.18) ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.18 - '@inquirer/type@3.0.6(@types/node@22.14.1)': + '@inquirer/type@3.0.6(@types/node@22.15.18)': optionalDependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.18 '@ipld/dag-cbor@9.2.2': dependencies: @@ -12327,7 +12339,7 @@ snapshots: dependencies: '@types/http-cache-semantics': 4.0.4 '@types/keyv': 3.1.4 - '@types/node': 22.14.1 + '@types/node': 22.15.18 '@types/responselike': 1.0.3 '@types/chai-as-promised@7.1.8': @@ -12350,7 +12362,7 @@ snapshots: '@types/dns-packet@5.6.5': dependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.18 '@types/estree@1.0.7': {} @@ -12372,7 +12384,7 @@ snapshots: '@types/keyv@3.1.4': dependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.18 '@types/lodash-es@4.17.12': dependencies: @@ -12395,19 +12407,23 @@ snapshots: '@types/multicast-dns@7.2.4': dependencies: '@types/dns-packet': 5.6.5 - '@types/node': 22.14.1 + '@types/node': 22.15.18 '@types/node@22.14.1': dependencies: undici-types: 6.21.0 + '@types/node@22.15.18': + dependencies: + undici-types: 6.21.0 + '@types/normalize-package-data@2.4.4': {} '@types/parse-json@4.0.2': {} '@types/responselike@1.0.3': dependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.18 '@types/retry@0.12.0': {} @@ -12431,7 +12447,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.18 '@types/yargs-parser@21.0.3': {} @@ -12445,7 +12461,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 22.14.1 + '@types/node': 22.15.18 optional: true '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)': @@ -12778,7 +12794,7 @@ snapshots: '@types/chai-string': 1.4.5 '@types/chai-subset': 1.3.6(@types/chai@4.3.20) '@types/mocha': 10.0.10 - '@types/node': 22.14.1 + '@types/node': 22.15.18 '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) buffer: 6.0.3 bytes: 3.1.2 @@ -12854,7 +12870,7 @@ snapshots: typedoc-plugin-mdn-links: 3.3.8(typedoc@0.25.13(typescript@5.6.3)) typedoc-plugin-missing-exports: 2.3.0(typedoc@0.25.13(typescript@5.6.3)) typescript: 5.6.3 - typescript-docs-verifier: 2.5.3(@types/node@22.14.1)(typescript@5.6.3) + typescript-docs-verifier: 2.5.3(@types/node@22.15.18)(typescript@5.6.3) wherearewe: 2.0.1 yargs: 17.7.2 yargs-parser: 21.1.1 @@ -19498,14 +19514,14 @@ snapshots: dependencies: typescript: 5.6.3 - ts-node@10.9.2(@types/node@22.14.1)(typescript@5.6.3): + ts-node@10.9.2(@types/node@22.15.18)(typescript@5.6.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.14.1 + '@types/node': 22.15.18 acorn: 8.14.1 acorn-walk: 8.3.4 arg: 4.1.3 @@ -19651,13 +19667,13 @@ snapshots: shiki: 0.14.7 typescript: 5.6.3 - typescript-docs-verifier@2.5.3(@types/node@22.14.1)(typescript@5.6.3): + typescript-docs-verifier@2.5.3(@types/node@22.15.18)(typescript@5.6.3): dependencies: chalk: 4.1.2 fs-extra: 10.1.0 ora: 5.4.1 strip-ansi: 7.1.0 - ts-node: 10.9.2(@types/node@22.14.1)(typescript@5.6.3) + ts-node: 10.9.2(@types/node@22.15.18)(typescript@5.6.3) tsconfig: 7.0.0 typescript: 5.6.3 yargs: 17.7.2 diff --git a/releases.example.json b/releases.example.json index c9c8700..d14f2c2 100644 --- a/releases.example.json +++ b/releases.example.json @@ -8,8 +8,7 @@ "cover": "QmcD4R3Qj8jBWY73H9LQWESgonNB1AMN3of23ubjDhJVSm", "metadata": { "classification": "PG", - "description": - "Join filmmaker Brett Gaylor and mashup artist Girl Talk as they explore copyright and content creation in the digital age. In the process they dissect the media landscape of the 21st century and shatter the wall between users and producers.", + "description": "Join filmmaker Brett Gaylor and mashup artist Girl Talk as they explore copyright and content creation in the digital age. In the process they dissect the media landscape of the 21st century and shatter the wall between users and producers.", "duration": "1h 26m" } }, @@ -22,8 +21,7 @@ "cover": "bafkreiemmzezvfmeueaeuqfewtf4d6fiuvjqnh4xulgwuugfknmh3abfxi", "metadata": { "classification": "PG", - "description": - "The Internet's Own Boy follows the story of programming prodigy and information activist Aaron Swartz.", + "description": "The Internet's Own Boy follows the story of programming prodigy and information activist Aaron Swartz.", "duration": "1h 45m", "releaseYear": "2014" } @@ -37,10 +35,9 @@ "cover": "bafkreiemqveqhpksefhup46d77iybtatf2vb2bgyak4hfydxaz5hxser34", "metadata": { "classification": "Unrated", - "description": - "The Pirate Bay Away From Keyboard is a documentary film about the file sharing website The Pirate Bay.", + "description": "The Pirate Bay Away From Keyboard is a documentary film about the file sharing website The Pirate Bay.", "duration": "1h 22m", "releaseYear": "2013" } } -] \ No newline at end of file +] diff --git a/releases/releases.movies.json b/releases/releases.movies.json index f5a56cd..babeffe 100644 --- a/releases/releases.movies.json +++ b/releases/releases.movies.json @@ -8,8 +8,7 @@ "cover": "QmcD4R3Qj8jBWY73H9LQWESgonNB1AMN3of23ubjDhJVSm", "metadata": { "classification": "PG", - "description": - "Join filmmaker Brett Gaylor and mashup artist Girl Talk as they explore copyright and content creation in the digital age. In the process they dissect the media landscape of the 21st century and shatter the wall between users and producers.", + "description": "Join filmmaker Brett Gaylor and mashup artist Girl Talk as they explore copyright and content creation in the digital age. In the process they dissect the media landscape of the 21st century and shatter the wall between users and producers.", "duration": "1h 26m" } }, @@ -22,8 +21,7 @@ "cover": "bafkreiemmzezvfmeueaeuqfewtf4d6fiuvjqnh4xulgwuugfknmh3abfxi", "metadata": { "classification": "PG", - "description": - "The Internet's Own Boy follows the story of programming prodigy and information activist Aaron Swartz.", + "description": "The Internet's Own Boy follows the story of programming prodigy and information activist Aaron Swartz.", "duration": "1h 45m", "releaseYear": "2014" } @@ -37,8 +35,7 @@ "cover": "bafkreiemqveqhpksefhup46d77iybtatf2vb2bgyak4hfydxaz5hxser34", "metadata": { "classification": "Unrated", - "description": - "The Pirate Bay Away From Keyboard is a documentary film about the file sharing website The Pirate Bay.", + "description": "The Pirate Bay Away From Keyboard is a documentary film about the file sharing website The Pirate Bay.", "duration": "1h 22m", "releaseYear": "2013" } @@ -67,4 +64,4 @@ "releaseYear": "1968" } } -] \ No newline at end of file +] diff --git a/releases/releases.music.json b/releases/releases.music.json index a5464c4..4dc41f7 100644 --- a/releases/releases.music.json +++ b/releases/releases.music.json @@ -7,4 +7,4 @@ "thumbnail": "QmcuyamRXLZ7CigNPqM52YyEUHRp5zRWB8MgXbsFBKynhn", "cover": "QmcuyamRXLZ7CigNPqM52YyEUHRp5zRWB8MgXbsFBKynhn" } -] \ No newline at end of file +] diff --git a/src/@types/abstract-level.d.ts b/src/@types/abstract-level.d.ts new file mode 100644 index 0000000..c993a59 --- /dev/null +++ b/src/@types/abstract-level.d.ts @@ -0,0 +1,74 @@ +declare module "abstract-level" { + export interface AbstractLevel { + // Basic interface for AbstractLevel + open( + options: Record, + callback: (error?: Error) => void, + ): void; + close(callback: (error?: Error) => void): void; + put( + key: K, + value: V, + options: Record, + callback: (error?: Error) => void, + ): void; + get( + key: K, + options: Record, + callback: (error?: Error, value?: V) => void, + ): void; + del( + key: K, + options: Record, + callback: (error?: Error) => void, + ): void; + batch( + operations: Array>, + options: Record, + callback: (error?: Error) => void, + ): void; + iterator(options: Record): unknown; + } +} + +declare module "level" { + import { AbstractLevel } from "abstract-level"; + + export interface LevelOptions { + keyEncoding?: string; + valueEncoding?: string; + db?: unknown; + [key: string]: unknown; + } + + export class Level implements AbstractLevel { + constructor(location: string, options?: LevelOptions); + open( + options: Record, + callback: (error?: Error) => void, + ): void; + close(callback: (error?: Error) => void): void; + put( + key: K, + value: V, + options: Record, + callback: (error?: Error) => void, + ): void; + get( + key: K, + options: Record, + callback: (error?: Error, value?: V) => void, + ): void; + del( + key: K, + options: Record, + callback: (error?: Error) => void, + ): void; + batch( + operations: Array>, + options: Record, + callback: (error?: Error) => void, + ): void; + iterator(options: Record): unknown; + } +} diff --git a/src/bin.ts b/src/bin.ts index f686705..24aef55 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -14,6 +14,8 @@ import { créerConstellation, } from "constl-ipa-fork"; +import "./orbitedb-hook.js"; + import { createOrbiter, setUpSite, validateCategories } from "@/orbiter.js"; import { configIsComplete, @@ -22,7 +24,12 @@ import { saveConfig, } from "@/config.js"; import { ConfigMode, Release, releasesFileSchema } from "./types.js"; -import { CONFIG_FILE_NAME, DEFAULT_ORBITER_DIR, DEFAULT_VARIABLE_IDS, RELEASES_METADATA_COLUMN } from "./consts.js"; +import { + CONFIG_FILE_NAME, + DEFAULT_ORBITER_DIR, + DEFAULT_VARIABLE_IDS, + RELEASES_METADATA_COLUMN, +} from "./consts.js"; import { confirm } from "@inquirer/prompts"; const MACHINE_PREFIX = "MACHINE MESSAGE:"; @@ -48,7 +55,7 @@ const followConnections = async ({ ipa }: { ipa: Constellation }) => { constellation: [], }; let now = Date.now(); - const peerID = await ipa.obtIdLibp2p() + const peerID = await ipa.obtIdLibp2p(); const { sfip } = await ipa.attendreSfipEtOrbite(); const fFinale = () => { @@ -103,17 +110,18 @@ yargs(hideBin(process.argv)) ["config [--dir ]"], "Configure Orbiter", (yargs) => { - return yargs.option("dir", { - alias: "d", - describe: "The directory of the Orbiter node.", - type: "string", - default: DEFAULT_ORBITER_DIR, - }) - .option("ignore-defaults", { - alias: "i", - description: "Ignore defaults and regenerate all configuration.", - type: "boolean", - }); + return yargs + .option("dir", { + alias: "d", + describe: "The directory of the Orbiter node.", + type: "string", + default: DEFAULT_ORBITER_DIR, + }) + .option("ignore-defaults", { + alias: "i", + description: "Ignore defaults and regenerate all configuration.", + type: "boolean", + }); }, async (argv) => { const wheel = ora(chalk.yellow(`Starting Orbiter...`)); @@ -126,9 +134,16 @@ yargs(hideBin(process.argv)) const existingConfig = await getConfig({ dir }); if (!argv.ignoreDefaults) - existingConfig.variableIds = {...DEFAULT_VARIABLE_IDS, ...existingConfig.variableIds} - const categoriesData = await validateCategories({ dir}); - const config = await setUpSite({ constellation, categoriesData, ...existingConfig }); + existingConfig.variableIds = { + ...DEFAULT_VARIABLE_IDS, + ...existingConfig.variableIds, + }; + const categoriesData = await validateCategories({ dir }); + const config = await setUpSite({ + constellation, + categoriesData, + ...existingConfig, + }); await saveConfig({ dir, config, mode: "json" }); await constellation.fermer(); wheel?.succeed( @@ -268,6 +283,7 @@ yargs(hideBin(process.argv)) let wheel: Ora | undefined = undefined; let forgetConnections: types.schémaFonctionOublier | undefined = undefined; + let stopLensServer: (() => Promise) | undefined = undefined; if (argv.machine) { sendMachineMessage({ message: { type: "STARTING ORBITER" } }); @@ -281,10 +297,17 @@ yargs(hideBin(process.argv)) domaines: argv.domain ? [argv.domain] : undefined, }); - await createOrbiter({ + const { orbiter } = await createOrbiter({ constellation, + databaseConfig: { + type: "rocksdb", + multiProcess: true, + }, }); + const { startLensServer } = await import("./lens-server.js"); + stopLensServer = startLensServer(orbiter, argv.dir); + process.stdin.on("data", async () => { if (argv.machine) { sendMachineMessage({ message: { type: "Closing Orbiter" } }); @@ -292,6 +315,7 @@ yargs(hideBin(process.argv)) wheel?.start(chalk.yellow("Closing Orbiter...")); } try { + await stopLensServer?.(); await forgetConnections?.(); await constellation.fermer(); } finally { @@ -337,13 +361,37 @@ yargs(hideBin(process.argv)) async (argv) => { if (!argv.account) throw new Error("Account must be specified."); - const wheel = ora(chalk.yellow(`Starting Orbiter`)); + const wheel = ora(chalk.yellow(`Checking for running lens...`)); + + // Check if a lens is running and use it if available + const { isLensRunning, authorizeUserThroughLens } = await import("./lens-client.js"); + const lensRunning = await isLensRunning(argv.dir); + + if (lensRunning) { + wheel.info(chalk.yellow("Found running lens, using it for authorization")); + + try { + wheel.start(chalk.yellow("Authorising account through running lens...")); + await authorizeUserThroughLens(argv.account, true, argv.dir); + wheel.succeed(chalk.yellow("All done!")); + process.exit(0); + } catch (error) { + wheel.fail(chalk.red(`Error authorising through running lens: ${error.message}`)); + wheel.info(chalk.yellow("Falling back to direct authorization...")); + } + } + + wheel.start(chalk.yellow(`Starting Orbiter for direct authorization`)); const constellation = créerConstellation({ dossier: argv.dir, }); const { orbiter } = await createOrbiter({ constellation, + databaseConfig: { + type: "rocksdb", + multiProcess: true, + }, }); wheel.start(chalk.yellow("Authorising account...")); @@ -389,6 +437,10 @@ yargs(hideBin(process.argv)) const { orbiter } = await createOrbiter({ constellation, + databaseConfig: { + type: "rocksdb", + multiProcess: true, + }, }); wheel.start(chalk.yellow("Authorising account...")); @@ -430,11 +482,15 @@ yargs(hideBin(process.argv)) const { orbiter } = await createOrbiter({ constellation, + databaseConfig: { + type: "rocksdb", + multiProcess: true, + }, }); wheel.start(chalk.yellow("Authorising account...")); await orbiter.untrustSite({ - siteId: argv.siteId + siteId: argv.siteId, }); wheel.start(chalk.yellow("Cleaning things up...")); @@ -462,53 +518,57 @@ yargs(hideBin(process.argv)) }, async (argv) => { if (!argv.file) throw new Error("JSON File path must be specified."); - + const wheel = ora(chalk.yellow(`Starting Orbiter`)); const constellation = créerConstellation({ dossier: argv.dir, }); - + const { orbiter } = await createOrbiter({ constellation, + databaseConfig: { + type: "rocksdb", + multiProcess: true, + }, }); - + // Check if JSON file exists and read it wheel.start(chalk.yellow("Reading and validating releases file...")); const fs = await import("fs"); const path = await import("path"); const Ajv = (await import("ajv")).default; - + const filePath = path.resolve(argv.file); if (!fs.existsSync(filePath)) { wheel.fail(chalk.red(`File not found: ${filePath}`)); await constellation.fermer(); process.exit(1); } - + let releasesData: Release>[]; try { const fileContent = fs.readFileSync(filePath, "utf8"); const jsonData = JSON.parse(fileContent); - + // Validate with AJV const ajv = new Ajv({ allErrors: true, allowUnionTypes: true }); const validate = ajv.compile(releasesFileSchema); const valid = validate(jsonData); - + if (!valid) { - const errors = validate.errors?.map(err => - `${err.instancePath}: ${err.message}` - ).join("\n"); + const errors = validate.errors + ?.map((err) => `${err.instancePath}: ${err.message}`) + .join("\n"); throw new Error(`Validation failed:\n${errors}`); } - + releasesData = jsonData as Release>[]; } catch (error) { wheel.fail(chalk.red(`Error processing JSON file: ${error.message}`)); await constellation.fermer(); process.exit(1); } - + // Upload releases wheel.start(chalk.yellow(`Uploading ${releasesData.length} releases...`)); try { @@ -522,15 +582,17 @@ yargs(hideBin(process.argv)) : undefined, }; await orbiter.addRelease(release); - }) + }), + ); + wheel.succeed( + chalk.yellow(`Successfully uploaded ${releasesData.length} releases`), ); - wheel.succeed(chalk.yellow(`Successfully uploaded ${releasesData.length} releases`)); } catch (error) { wheel.fail(chalk.red(`Error uploading releases: ${error.message}`)); await constellation.fermer(); process.exit(1); } - + // Cleanup wheel.start(chalk.yellow("Cleaning things up...")); await constellation.fermer(); @@ -544,19 +606,20 @@ yargs(hideBin(process.argv)) "Delete releases by their IDs", (yargs) => { return yargs - .option("dir", { - alias: "d", - describe: "The directory of the Orbiter node.", - type: "string", - default: DEFAULT_ORBITER_DIR, - }) - .option("releases-ids", { - alias: "ids", - describe: "IDs of the releases to delete (can be specified multiple times)", - type: "string", - array: true, - demandOption: true, - }); + .option("dir", { + alias: "d", + describe: "The directory of the Orbiter node.", + type: "string", + default: DEFAULT_ORBITER_DIR, + }) + .option("releases-ids", { + alias: "ids", + describe: + "IDs of the releases to delete (can be specified multiple times)", + type: "string", + array: true, + demandOption: true, + }); }, async (argv) => { const wheel = ora(chalk.yellow(`Starting Orbiter`)); @@ -564,26 +627,36 @@ yargs(hideBin(process.argv)) const releasesIds = argv.releasesIds; if (!releasesIds || releasesIds.length === 0) { - wheel.fail(chalk.red("At least one release ID must be provided using --releases-ids or -ids.")); - process.exit(1); + wheel.fail( + chalk.red( + "At least one release ID must be provided using --releases-ids or -ids.", + ), + ); + process.exit(1); } const constellation = créerConstellation({ dossier: dir, }); - + try { const { orbiter } = await createOrbiter({ constellation, + databaseConfig: { + type: "rocksdb", + multiProcess: true, + }, }); wheel.start(chalk.yellow(`Deleting ${releasesIds.length} releases...`)); await Promise.all( releasesIds.map(async (releaseId) => { await orbiter.removeRelease(releaseId); - }) + }), + ); + wheel.succeed( + chalk.yellow(`Successfully deleted ${releasesIds.length} releases`), ); - wheel.succeed(chalk.yellow(`Successfully deleted ${releasesIds.length} releases`)); } catch (error) { wheel.fail(chalk.red(`Error deteling releases: ${error.message}`)); await constellation.fermer(); diff --git a/src/constellation-patch.ts b/src/constellation-patch.ts new file mode 100644 index 0000000..506aeb8 --- /dev/null +++ b/src/constellation-patch.ts @@ -0,0 +1,36 @@ +import { DatabaseConfig } from "./types"; +import fs from "fs"; +import path from "path"; + +let globalDbConfig: DatabaseConfig | undefined; + +/** + * Patches the Constellation library to use RocksDB with multi-process support + * This must be called before initializing Constellation + */ +export function patchConstellationConfig(dbConfig: DatabaseConfig): void { + globalDbConfig = dbConfig; + + const configDir = + process.env.ORBITER_CONFIG_DIR || path.join(process.cwd(), ".orbiter"); + + if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); + } + + const configPath = path.join(configDir, "db-config.json"); + fs.writeFileSync(configPath, JSON.stringify(dbConfig)); + + process.env.ORBITDB_ROCKSDB_MULTIPROCESS = dbConfig.multiProcess + ? "true" + : "false"; + process.env.ORBITDB_STORAGE_ADAPTER = + dbConfig.type === "rocksdb" ? "leveldb" : "level"; +} + +/** + * Gets the current global database configuration + */ +export function getGlobalDbConfig(): DatabaseConfig | undefined { + return globalDbConfig; +} diff --git a/src/consts.ts b/src/consts.ts index e8abee3..2936189 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -41,260 +41,305 @@ export const CONFIG_FILE_NAME = ".orbiter-config.json"; export const DEFAULT_ORBITER_DIR = ".orbiter"; export const DEFAULT_VARIABLE_IDS = { - trustedSitesSiteIdVar: "/orbitdb/zdpuB2wkkeLz6ydHKpsS77vDNRHbasD8JYExS9hY4jBVZqKkf", - trustedSitesNameVar: "/orbitdb/zdpuArhaMLFcttwWrd5RuHgfS4tV9GcitBKcNwarvQLr9VDFi", - featuredReleasesReleaseIdVar: "/orbitdb/zdpuAuoH7hoTZvBuAxG667k5YMSZaCQXU8Y88CoRG57vjZLaZ", - featuredReleasesStartTimeVar: "/orbitdb/zdpuArQnMwqU1jN9eZ9J6k8H1euLRF67qVkeWjU9NBTAf4HLQ", - featuredReleasesEndTimeVar: "/orbitdb/zdpuArhQVb8NYLXu8oyEbKZmdwGmPWXACxrYBR49zGkGyo2NV", - featuredReleasesPromotedVar: "/orbitdb/zdpuAzjr3v1PjM33JUp7NJpKtYMNeYQJwiR5K3REKeSCMoegq", - blockedReleasesReleaseIdVar: "/orbitdb/zdpuAkyAA7XiPurjxZ39b2s5Vt57Z5dqnr6TSgMap5wP1qYBi", - contentCategoriesCategoryIdVar: "/orbitdb/zdpuAtrNCjNSKf8hBM6rrqC4prVvsNph3Wt7E4Y2W8pFvGDXx", - contentCategoriesDisplayNameVar: "/orbitdb/zdpuAzKVn6Ep8feK8uCtu9GFJGPK2Z3mEmPcg2STKAuGEsKdz", - contentCategoriesFeaturedVar: "/orbitdb/zdpuAwmgAbZLeGke6Gaf4tiyw5Rk863nv9BNuwrcezWSKWY7J", - contentCategoriesMetadataSchemaVar: "/orbitdb/zdpuAnJewUq9HZwExebbauea47Fw2v5JWtxhVufn3UZZBhTsW", + trustedSitesSiteIdVar: + "/orbitdb/zdpuB2wkkeLz6ydHKpsS77vDNRHbasD8JYExS9hY4jBVZqKkf", + trustedSitesNameVar: + "/orbitdb/zdpuArhaMLFcttwWrd5RuHgfS4tV9GcitBKcNwarvQLr9VDFi", + featuredReleasesReleaseIdVar: + "/orbitdb/zdpuAuoH7hoTZvBuAxG667k5YMSZaCQXU8Y88CoRG57vjZLaZ", + featuredReleasesStartTimeVar: + "/orbitdb/zdpuArQnMwqU1jN9eZ9J6k8H1euLRF67qVkeWjU9NBTAf4HLQ", + featuredReleasesEndTimeVar: + "/orbitdb/zdpuArhQVb8NYLXu8oyEbKZmdwGmPWXACxrYBR49zGkGyo2NV", + featuredReleasesPromotedVar: + "/orbitdb/zdpuAzjr3v1PjM33JUp7NJpKtYMNeYQJwiR5K3REKeSCMoegq", + blockedReleasesReleaseIdVar: + "/orbitdb/zdpuAkyAA7XiPurjxZ39b2s5Vt57Z5dqnr6TSgMap5wP1qYBi", + contentCategoriesCategoryIdVar: + "/orbitdb/zdpuAtrNCjNSKf8hBM6rrqC4prVvsNph3Wt7E4Y2W8pFvGDXx", + contentCategoriesDisplayNameVar: + "/orbitdb/zdpuAzKVn6Ep8feK8uCtu9GFJGPK2Z3mEmPcg2STKAuGEsKdz", + contentCategoriesFeaturedVar: + "/orbitdb/zdpuAwmgAbZLeGke6Gaf4tiyw5Rk863nv9BNuwrcezWSKWY7J", + contentCategoriesMetadataSchemaVar: + "/orbitdb/zdpuAnJewUq9HZwExebbauea47Fw2v5JWtxhVufn3UZZBhTsW", releasesFileVar: "/orbitdb/zdpuAopp994ERCjk8Gb8D6m1kqSk5RoW9mq1Xsb9yogqgdGcX", - releasesAuthorVar: "/orbitdb/zdpuAwK1RDcoYe9XxN7G3eohtC6xrquybseqNUqsJMuMA1gKN", - releasesContentNameVar: "/orbitdb/zdpuB1DGtFGEJDVNzxQuVzixjzT3MNH2cpxqBZBcVgDXVt17j", - releasesThumbnailVar: "/orbitdb/zdpuAxWrRcBpPYkmBXr7XQLVB721ZbBomncpKf7UMrGgVxqau", - releasesCoverVar: "/orbitdb/zdpuAx9oHfCHQ4AuMGjACVi9nze5wfKDW9h6NzXVyfv1ZV93t", - releasesMetadataVar: "/orbitdb/zdpuAwPUmxyTjE26e4LLMNFtnQvaaUeVyQczS2GQjX6MaWzXu", - releasesCategoryVar: "/orbitdb/zdpuArKo58rLT9WawJScfBMh83H68UXrgdVC2FCvwCZVqJ7fA", - collectionsAuthorVar: "/orbitdb/zdpuAvM6HdN4tRWMTC2Gyqx9WhgiT3xap2N3cK5JLQUqrxeLS", - collectionsMetadataVar: "/orbitdb/zdpuAuScb292oiKL8DZsdPcQieFbUo64Nz8UMy8W2r6omv6f3", - collectionsNameVar: "/orbitdb/zdpuB29yexzEQ8xjFHWU5WPnGsR74cNVHRyp2ynKHFLr3ycsh", - collectionsThumbnailVar: "/orbitdb/zdpuAv4xkPWt2sy8uRRsyqvUK18nkJgR23MbEd85FRbtiG73k", - collectionsReleasesVar: "/orbitdb/zdpuAt2rnT1gYeQxgVcL6B35k7c7JEWWUTtNTQ8xxous6a13N", - collectionsCategoryVar: "/orbitdb/zdpuAvAND67Sjsc2csTnJZEdJ5xURyT79PpB5gWX6fVjixvak" -} + releasesAuthorVar: + "/orbitdb/zdpuAwK1RDcoYe9XxN7G3eohtC6xrquybseqNUqsJMuMA1gKN", + releasesContentNameVar: + "/orbitdb/zdpuB1DGtFGEJDVNzxQuVzixjzT3MNH2cpxqBZBcVgDXVt17j", + releasesThumbnailVar: + "/orbitdb/zdpuAxWrRcBpPYkmBXr7XQLVB721ZbBomncpKf7UMrGgVxqau", + releasesCoverVar: + "/orbitdb/zdpuAx9oHfCHQ4AuMGjACVi9nze5wfKDW9h6NzXVyfv1ZV93t", + releasesMetadataVar: + "/orbitdb/zdpuAwPUmxyTjE26e4LLMNFtnQvaaUeVyQczS2GQjX6MaWzXu", + releasesCategoryVar: + "/orbitdb/zdpuArKo58rLT9WawJScfBMh83H68UXrgdVC2FCvwCZVqJ7fA", + collectionsAuthorVar: + "/orbitdb/zdpuAvM6HdN4tRWMTC2Gyqx9WhgiT3xap2N3cK5JLQUqrxeLS", + collectionsMetadataVar: + "/orbitdb/zdpuAuScb292oiKL8DZsdPcQieFbUo64Nz8UMy8W2r6omv6f3", + collectionsNameVar: + "/orbitdb/zdpuB29yexzEQ8xjFHWU5WPnGsR74cNVHRyp2ynKHFLr3ycsh", + collectionsThumbnailVar: + "/orbitdb/zdpuAv4xkPWt2sy8uRRsyqvUK18nkJgR23MbEd85FRbtiG73k", + collectionsReleasesVar: + "/orbitdb/zdpuAt2rnT1gYeQxgVcL6B35k7c7JEWWUTtNTQ8xxous6a13N", + collectionsCategoryVar: + "/orbitdb/zdpuAvAND67Sjsc2csTnJZEdJ5xURyT79PpB5gWX6fVjixvak", +}; -export const DEFAULT_CONTENT_CATEGORIES: ContentCategory[] = [ - { - categoryId: "music", - displayName: "Music", - featured: true, - metadataSchema: { - description: { - type: "string", - description: "Brief description of the music content" - }, - totalSongs: { - type: "number", - description: "Total number of songs in this category" - }, - totalDuration: { - type: "string", - description: "Total duration of all songs (e.g., in HH:MM:SS format)" - }, - genres: { - type: "array", - description: "List of genres represented in this category" - }, - tags: { - type: "string", - description: "Tags associated with the music release" - }, - musicBrainzID: { - type: "string", - description: "MusicBrainz identifier for the release" - }, - albumTitle: { - type: "string", - description: "Title of the album" - }, - releaseYear: { - type: "number", - description: "Year of release" - }, - releaseType: { - type: "string", - description: "Type of music release", - options: [ - 'Album', - 'Soundtrack', - 'EP', - 'Anthology', - 'Compilation', - 'Single', - 'Live Album', - 'Remix', - 'Bootleg', - 'Interview', - 'Mixtape', - 'Demo', - 'Concert Recording', - 'DJ Mix', - 'Unknown', - ], - }, - fileFormat: { - type: "string", - description: "Audio file format", - options: ['MP3', 'FLAC', 'AAC', 'AC3', 'DTS'], - }, - bitrate: { - type: "string", - description: "Audio bitrate (e.g., 320kbps)" - }, - mediaFormat: { - type: "string", - description: "Physical media format if applicable", - options: ['CD', 'DVD', 'Vinyl', 'Soundboard', 'SACD', 'DAT', 'WEB', 'Blu-Ray'], - } - } - }, - { - categoryId: "video", - displayName: "Videos", - metadataSchema: { - title: { - type: "string", - description: "Title of the video" - }, - description: { - type: "string", - description: "Brief description of the video content" - }, - duration: { - type: "string", - description: "Length of the video (e.g., HH:MM:SS)" - }, - resolution: { - type: "string", - description: "Video resolution (e.g., 1920x1080)" - }, - format: { - type: "string", - description: "File format of the video (e.g., mp4, mov)" - }, - tags: { - type: "array", - description: "User-defined tags for searchability (e.g., tutorial, vlog, funny)" - }, - uploader: { - type: "string", - description: "Name or ID of the uploader/creator" - }, - uploadDate: { - type: "string", - description: "Date the video was uploaded (e.g., YYYY-MM-DD)" - }, - sourceUrl: { - type: "string", - description: "Original URL if sourced from an online platform (e.g., YouTube link)" - } - } - }, - { - categoryId: "movie", - displayName: "Movies", - featured: true, - metadataSchema: { - description: { - type: "string", - description: "Brief description of the movie" - }, - resolution: { - type: "string", - description: "Video resolution (e.g., 1920x1080)" - }, - format: { - type: "string", - description: "File format of the video (e.g., mp4, mov)" - }, - genres: { - type: "array", - description: "Genres associated with the video (e.g., action, drama)" - }, - tags: { - type: "array", - description: "User-defined tags for searchability (e.g., funny, tutorial)" - }, - posterCID: { - type: "string", - description: "Content ID for the movie poster" - }, - TMDBID: { - type: "string", - description: "The Movie Database identifier" - }, - IMDBID: { - type: "string", - description: "Internet Movie Database identifier" - }, - releaseType: { - type: "string", - description: "Type of movie release" - }, - releaseYear: { - type: "number", - description: "Year of release" - }, - classification: { - type: "string", - description: "Content rating/classification (e.g., PG-13)" - }, - duration: { - type: "string", - description: "Length of the movie" - } - } - }, - { - categoryId: "tvShow", - displayName: "TV Shows", - featured: true, - metadataSchema: { - description: { - type: "string", - description: "Brief description of the TV show" - }, - seasons: { - type: "number", - description: "Number of seasons in the TV show" - }, - totalEpisodes: { - type: "number", - description: "Total number of episodes aired across all seasons" - }, - genres: { - type: "array", - description: "Genres associated with the TV show (e.g., comedy, sci-fi)" - }, - firstAiredYear: { - type: "number", - description: "Year the TV show first aired" - }, - status: { - type: "string", - description: "Current status of the TV show", - options: ['Returning Series', 'Ended', 'Canceled', 'In Production', 'Pilot', 'Unknown'], - }, - TMDBID: { - type: "string", - description: "The Movie Database identifier for the TV show" - }, - IMDBID: { - type: "string", - description: "Internet Movie Database identifier for the TV show" - }, - posterCID: { - type: "string", - description: "Content ID for the TV show poster" - }, - classification: { - type: "string", - description: "Content rating/classification (e.g., TV-MA, TV-14)" - }, - network: { - type: "string", - description: "Original television network or streaming service" - }, - averageEpisodeDuration: { - type: "string", - description: "Average duration of an episode (e.g., ~45 min, 00:45:00)" - } - } - } -] +export const DEFAULT_CONTENT_CATEGORIES: ContentCategory[] = + [ + { + categoryId: "music", + displayName: "Music", + featured: true, + metadataSchema: { + description: { + type: "string", + description: "Brief description of the music content", + }, + totalSongs: { + type: "number", + description: "Total number of songs in this category", + }, + totalDuration: { + type: "string", + description: "Total duration of all songs (e.g., in HH:MM:SS format)", + }, + genres: { + type: "array", + description: "List of genres represented in this category", + }, + tags: { + type: "string", + description: "Tags associated with the music release", + }, + musicBrainzID: { + type: "string", + description: "MusicBrainz identifier for the release", + }, + albumTitle: { + type: "string", + description: "Title of the album", + }, + releaseYear: { + type: "number", + description: "Year of release", + }, + releaseType: { + type: "string", + description: "Type of music release", + options: [ + "Album", + "Soundtrack", + "EP", + "Anthology", + "Compilation", + "Single", + "Live Album", + "Remix", + "Bootleg", + "Interview", + "Mixtape", + "Demo", + "Concert Recording", + "DJ Mix", + "Unknown", + ], + }, + fileFormat: { + type: "string", + description: "Audio file format", + options: ["MP3", "FLAC", "AAC", "AC3", "DTS"], + }, + bitrate: { + type: "string", + description: "Audio bitrate (e.g., 320kbps)", + }, + mediaFormat: { + type: "string", + description: "Physical media format if applicable", + options: [ + "CD", + "DVD", + "Vinyl", + "Soundboard", + "SACD", + "DAT", + "WEB", + "Blu-Ray", + ], + }, + }, + }, + { + categoryId: "video", + displayName: "Videos", + metadataSchema: { + title: { + type: "string", + description: "Title of the video", + }, + description: { + type: "string", + description: "Brief description of the video content", + }, + duration: { + type: "string", + description: "Length of the video (e.g., HH:MM:SS)", + }, + resolution: { + type: "string", + description: "Video resolution (e.g., 1920x1080)", + }, + format: { + type: "string", + description: "File format of the video (e.g., mp4, mov)", + }, + tags: { + type: "array", + description: + "User-defined tags for searchability (e.g., tutorial, vlog, funny)", + }, + uploader: { + type: "string", + description: "Name or ID of the uploader/creator", + }, + uploadDate: { + type: "string", + description: "Date the video was uploaded (e.g., YYYY-MM-DD)", + }, + sourceUrl: { + type: "string", + description: + "Original URL if sourced from an online platform (e.g., YouTube link)", + }, + }, + }, + { + categoryId: "movie", + displayName: "Movies", + featured: true, + metadataSchema: { + description: { + type: "string", + description: "Brief description of the movie", + }, + resolution: { + type: "string", + description: "Video resolution (e.g., 1920x1080)", + }, + format: { + type: "string", + description: "File format of the video (e.g., mp4, mov)", + }, + genres: { + type: "array", + description: "Genres associated with the video (e.g., action, drama)", + }, + tags: { + type: "array", + description: + "User-defined tags for searchability (e.g., funny, tutorial)", + }, + posterCID: { + type: "string", + description: "Content ID for the movie poster", + }, + TMDBID: { + type: "string", + description: "The Movie Database identifier", + }, + IMDBID: { + type: "string", + description: "Internet Movie Database identifier", + }, + releaseType: { + type: "string", + description: "Type of movie release", + }, + releaseYear: { + type: "number", + description: "Year of release", + }, + classification: { + type: "string", + description: "Content rating/classification (e.g., PG-13)", + }, + duration: { + type: "string", + description: "Length of the movie", + }, + }, + }, + { + categoryId: "tvShow", + displayName: "TV Shows", + featured: true, + metadataSchema: { + description: { + type: "string", + description: "Brief description of the TV show", + }, + seasons: { + type: "number", + description: "Number of seasons in the TV show", + }, + totalEpisodes: { + type: "number", + description: "Total number of episodes aired across all seasons", + }, + genres: { + type: "array", + description: + "Genres associated with the TV show (e.g., comedy, sci-fi)", + }, + firstAiredYear: { + type: "number", + description: "Year the TV show first aired", + }, + status: { + type: "string", + description: "Current status of the TV show", + options: [ + "Returning Series", + "Ended", + "Canceled", + "In Production", + "Pilot", + "Unknown", + ], + }, + TMDBID: { + type: "string", + description: "The Movie Database identifier for the TV show", + }, + IMDBID: { + type: "string", + description: "Internet Movie Database identifier for the TV show", + }, + posterCID: { + type: "string", + description: "Content ID for the TV show poster", + }, + classification: { + type: "string", + description: "Content rating/classification (e.g., TV-MA, TV-14)", + }, + network: { + type: "string", + description: "Original television network or streaming service", + }, + averageEpisodeDuration: { + type: "string", + description: + "Average duration of an episode (e.g., ~45 min, 00:45:00)", + }, + }, + }, + ]; -export const RIFFCC_PROTOCOL = '/riffcc/1.0.0'; \ No newline at end of file +export const RIFFCC_PROTOCOL = "/riffcc/1.0.0"; diff --git a/src/index.ts b/src/index.ts index 294402e..f6af1aa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,8 @@ export { configIsComplete } from "./config.js"; export { version } from "./version.js"; export * as types from "./types.js"; export * as consts from "./consts.js"; +export { configureOrbitDBStore } from "./orbitedb-hook.js"; +export { patchConstellationConfig } from "./constellation-patch.js"; +export { retryDbOperation } from "./utils/db-retry.js"; +export { isLensRunning, authorizeUserThroughLens } from "./lens-client.js"; +export { startLensServer } from "./lens-server.js"; diff --git a/src/lens-client.ts b/src/lens-client.ts new file mode 100644 index 0000000..b97892f --- /dev/null +++ b/src/lens-client.ts @@ -0,0 +1,133 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as net from "net"; +import { DEFAULT_ORBITER_DIR } from "./consts.js"; + +/** + * Interface for lens commands + */ +export interface LensCommand { + type: string; + payload?: any; +} + +/** + * Interface for lens responses + */ +export interface LensResponse { + success: boolean; + message?: string; + error?: string; +} + +/** + * Checks if a lens is running in the specified directory + * @param dir Directory of the Orbiter node + * @returns True if a lens is running, false otherwise + */ +export async function isLensRunning(dir: string = DEFAULT_ORBITER_DIR): Promise { + const pidFile = path.join(dir, "lens.pid"); + + if (!fs.existsSync(pidFile)) { + return false; + } + + try { + const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10); + + process.kill(pid, 0); + return true; + } catch (error) { + try { + fs.unlinkSync(pidFile); + } catch (e) { + } + return false; + } +} + +/** + * Gets the socket path for the lens running in the specified directory + * @param dir Directory of the Orbiter node + * @returns Path to the lens socket + */ +export function getLensSocketPath(dir: string = DEFAULT_ORBITER_DIR): string { + return path.join(dir, "lens.sock"); +} + +/** + * Sends a command to a running lens + * @param command Command to send + * @param dir Directory of the Orbiter node + * @returns Promise that resolves with the response from the lens + */ +export function sendCommandToLens( + command: LensCommand, + dir: string = DEFAULT_ORBITER_DIR +): Promise { + return new Promise((resolve, reject) => { + const socketPath = getLensSocketPath(dir); + + if (!fs.existsSync(socketPath)) { + reject(new Error(`Lens socket not found at ${socketPath}`)); + return; + } + + const client = net.createConnection({ path: socketPath }, () => { + client.write(JSON.stringify(command)); + }); + + let data = ""; + + client.on("data", (chunk) => { + data += chunk.toString(); + }); + + client.on("end", () => { + try { + const response = JSON.parse(data) as LensResponse; + resolve(response); + } catch (error) { + reject(new Error(`Failed to parse response: ${error.message}`)); + } + }); + + client.on("error", (error) => { + reject(new Error(`Failed to connect to lens: ${error.message}`)); + }); + }); +} + +/** + * Authorizes a user through a running lens + * @param userId ID of the user to authorize + * @param admin Whether to make the user an admin + * @param dir Directory of the Orbiter node + * @returns Promise that resolves when the user is authorized + */ +export async function authorizeUserThroughLens( + userId: string, + admin: boolean = true, + dir: string = DEFAULT_ORBITER_DIR +): Promise { + const isRunning = await isLensRunning(dir); + + if (!isRunning) { + throw new Error("No lens is running. Start a lens with 'orb run' first."); + } + + const response = await sendCommandToLens( + { + type: "authorizeUser", + payload: { + userId, + admin, + }, + }, + dir + ); + + if (!response.success) { + throw new Error(response.error || "Failed to authorize user"); + } +} diff --git a/src/lens-server.ts b/src/lens-server.ts new file mode 100644 index 0000000..d162f5f --- /dev/null +++ b/src/lens-server.ts @@ -0,0 +1,96 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as net from "net"; +import { Orbiter } from "./orbiter.js"; +import { DEFAULT_ORBITER_DIR } from "./consts.js"; +import { LensCommand, LensResponse, getLensSocketPath } from "./lens-client.js"; + +/** + * Starts a lens server that listens for commands + * @param orbiter Orbiter instance + * @param dir Directory of the Orbiter node + * @returns Function to stop the server + */ +export function startLensServer( + orbiter: Orbiter, + dir: string = DEFAULT_ORBITER_DIR +): () => Promise { + const socketPath = getLensSocketPath(dir); + + if (fs.existsSync(socketPath)) { + fs.unlinkSync(socketPath); + } + + const server = net.createServer(async (socket) => { + let data = ""; + + socket.on("data", async (chunk) => { + data += chunk.toString(); + + try { + const command = JSON.parse(data) as LensCommand; + let response: LensResponse; + + switch (command.type) { + case "authorizeUser": + try { + await orbiter.inviteModerator({ + userId: command.payload.userId, + admin: command.payload.admin, + }); + + response = { + success: true, + message: `User ${command.payload.userId} authorized successfully`, + }; + } catch (error) { + response = { + success: false, + error: `Failed to authorize user: ${error.message}`, + }; + } + break; + + default: + response = { + success: false, + error: `Unknown command type: ${command.type}`, + }; + } + + socket.end(JSON.stringify(response)); + } catch (error) { + const response: LensResponse = { + success: false, + error: `Failed to parse command: ${error.message}`, + }; + + socket.end(JSON.stringify(response)); + } + }); + }); + + server.listen(socketPath); + + const pidFile = path.join(dir, "lens.pid"); + fs.writeFileSync(pidFile, process.pid.toString()); + + return async () => { + return new Promise((resolve) => { + server.close(() => { + try { + if (fs.existsSync(socketPath)) { + fs.unlinkSync(socketPath); + } + + if (fs.existsSync(pidFile)) { + fs.unlinkSync(pidFile); + } + } catch (e) { + } + + resolve(); + }); + }); + }; +} diff --git a/src/orbitedb-hook.ts b/src/orbitedb-hook.ts new file mode 100644 index 0000000..c731be9 --- /dev/null +++ b/src/orbitedb-hook.ts @@ -0,0 +1,31 @@ +import fs from "fs"; +import path from "path"; +import { DatabaseConfig } from "./types"; + +/** + * Configures OrbitDB to use our database configuration + */ +export function configureOrbitDBStore(): void { + try { + const configDir = + process.env.ORBITER_CONFIG_DIR || path.join(process.cwd(), ".orbiter"); + const configPath = path.join(configDir, "db-config.json"); + + if (fs.existsSync(configPath)) { + const config = JSON.parse( + fs.readFileSync(configPath, "utf-8"), + ) as DatabaseConfig; + + if (config.type === "rocksdb") { + process.env.ORBITDB_ROCKSDB_MULTIPROCESS = config.multiProcess + ? "true" + : "false"; + process.env.ORBITDB_STORAGE_ADAPTER = "leveldb"; + } + } + } catch (err) { + console.error("Error configuring OrbitDB store:", err); + } +} + +configureOrbitDBStore(); diff --git a/src/orbiter.ts b/src/orbiter.ts index 761e497..b1f3f91 100644 --- a/src/orbiter.ts +++ b/src/orbiter.ts @@ -3,6 +3,7 @@ import { TypedEmitter } from "tiny-typed-emitter"; import { Lock } from "semaphore-async-await"; import type { Constellation, bds, tableaux, types } from "constl-ipa-fork"; +import { DatabaseConfig } from "./types"; import { faisRien, ignorerNonDéfinis, @@ -63,6 +64,7 @@ import { categoriesFileSchema, variableIdKeys } from "./types.js"; import { removeUndefined } from "./utils.js"; import { configIsComplete, getConfig } from "./config.js"; import Ajv from "ajv"; +import { retryDbOperation } from "./utils/db-retry.js"; type forgetFunction = () => Promise; @@ -71,6 +73,16 @@ interface OrbiterEvents { siteId: string; variableIds: VariableIds; }) => void; + "user role changed": (args: { + userId: string; + role: "ADMIN" | "MODERATOR" | undefined; + }) => void; + "release added": (args: { releaseId: string; release: Release }) => void; + "release removed": (args: { releaseId: string }) => void; + "collection added": (args: { + collectionId: string; + collection: Collection; + }) => void; } type RootDbSchema = { @@ -205,12 +217,12 @@ const getSwarmDbSchema = ({ }; }; -export const validateCategories = async ({ dir } : { dir: string }) => { +export const validateCategories = async ({ dir }: { dir: string }) => { let categoriesData = DEFAULT_CONTENT_CATEGORIES; const { readFileSync, existsSync } = await import("fs"); const { join } = await import("path"); const categoriesPath = join(dir, "contentCategories.json"); - + if (existsSync(categoriesPath)) { categoriesData = JSON.parse(readFileSync(categoriesPath, "utf8")); console.log(JSON.stringify(categoriesData, null, 2)); @@ -234,7 +246,7 @@ export const setUpSite = async ({ constellation: Constellation; categoriesData: ContentCategory[]; siteId?: string; - variableIds?: PossiblyIncompleteVariableIds + variableIds?: PossiblyIncompleteVariableIds; }) => { // Variables for moderation database const trustedSitesSiteIdVar = @@ -262,7 +274,7 @@ export const setUpSite = async ({ (await constellation.variables.créerVariable({ catégorie: "horoDatage", })); - const featuredReleasesPromotedVar = + const featuredReleasesPromotedVar = variableIds.featuredReleasesPromotedVar || (await constellation.variables.créerVariable({ catégorie: "booléen", @@ -272,22 +284,22 @@ export const setUpSite = async ({ (await constellation.variables.créerVariable({ catégorie: "chaîneNonTraductible", })); - const contentCategoriesCategoryIdVar = + const contentCategoriesCategoryIdVar = variableIds.contentCategoriesCategoryIdVar || (await constellation.variables.créerVariable({ catégorie: "chaîneNonTraductible", })); - const contentCategoriesDisplayNameVar = + const contentCategoriesDisplayNameVar = variableIds.contentCategoriesDisplayNameVar || (await constellation.variables.créerVariable({ catégorie: "chaîneNonTraductible", })); - const contentCategoriesFeaturedVar = + const contentCategoriesFeaturedVar = variableIds.contentCategoriesFeaturedVar || (await constellation.variables.créerVariable({ catégorie: "booléen", })); - const contentCategoriesMetadataSchemaVar = + const contentCategoriesMetadataSchemaVar = variableIds.contentCategoriesMetadataSchemaVar || (await constellation.variables.créerVariable({ catégorie: "chaîneNonTraductible", @@ -325,10 +337,10 @@ export const setUpSite = async ({ catégorie: "chaîneNonTraductible", })); const releasesCategoryVar = - variableIds.releasesCategoryVar || - (await constellation.variables.créerVariable({ - catégorie: "chaîneNonTraductible", - })); + variableIds.releasesCategoryVar || + (await constellation.variables.créerVariable({ + catégorie: "chaîneNonTraductible", + })); // Variables for collections table const collectionsNameVar = @@ -363,11 +375,13 @@ export const setUpSite = async ({ })); // Swarm ID for site - let swarmId = siteId ? await constellation.orbite.appliquerFonctionBdOrbite({ - idBd: siteId, - fonction: "get", - args: ["swarmId"], - }) : undefined; + let swarmId = siteId + ? await constellation.orbite.appliquerFonctionBdOrbite({ + idBd: siteId, + fonction: "get", + args: ["swarmId"], + }) + : undefined; if (!swarmId) { swarmId = await constellation.nuées.créerNuée({}); @@ -404,11 +418,13 @@ export const setUpSite = async ({ } } - let modDbId = siteId ? await constellation.orbite.appliquerFonctionBdOrbite({ - idBd: siteId, - fonction: "get", - args: ["modDb"], - }) : undefined; + let modDbId = siteId + ? await constellation.orbite.appliquerFonctionBdOrbite({ + idBd: siteId, + fonction: "get", + args: ["modDb"], + }) + : undefined; if (!modDbId) { modDbId = await constellation.bds.créerBdDeSchéma({ schéma: { @@ -476,8 +492,8 @@ export const setUpSite = async ({ idColonne: CONTENT_CATEGORIES_METADATA_SCHEMA, }, ], - clef: CONTENT_CATEGORIES_TABLE_KEY - } + clef: CONTENT_CATEGORIES_TABLE_KEY, + }, ], }, }); @@ -485,10 +501,12 @@ export const setUpSite = async ({ const vals: types.élémentsBd = { [CONTENT_CATEGORIES_CATEGORY_ID]: category.categoryId, [CONTENT_CATEGORIES_DISPLAY_NAME]: category.displayName, - [CONTENT_CATEGORIES_METADATA_SCHEMA]: JSON.stringify(category.metadataSchema), - } + [CONTENT_CATEGORIES_METADATA_SCHEMA]: JSON.stringify( + category.metadataSchema, + ), + }; if (category.featured) { - vals[CONTENT_CATEGORIES_FEATURED] = category.featured + vals[CONTENT_CATEGORIES_FEATURED] = category.featured; } await constellation.bds.ajouterÉlémentÀTableauParClef({ idBd: modDbId, @@ -508,7 +526,7 @@ export const setUpSite = async ({ featuredReleasesStartTimeVar, featuredReleasesEndTimeVar, featuredReleasesPromotedVar, - + // blocked releases blockedReleasesReleaseIdVar, @@ -572,6 +590,7 @@ export class Orbiter { siteId: string; variableIds: VariableIds; + database?: DatabaseConfig; constellation: Constellation; events: TypedEmitter; @@ -581,15 +600,17 @@ export class Orbiter { siteId, variableIds, constellation, + database, }: { siteId: string; constellation: Constellation; variableIds?: VariableIds; + database?: DatabaseConfig; }) { this.events = new TypedEmitter(); this.siteId = siteId; - + this.database = database; this.variableIds = variableIds ?? DEFAULT_VARIABLE_IDS; this.constellation = constellation; @@ -599,7 +620,9 @@ export class Orbiter { async _init() { const { swarmId, modDbId } = await this.orbiterConfig(); - // await this.constellation.attendreInitialisée() + + await this.constellation.attendreInitialisée(); + this.forgetFns.push( await this.constellation.suivreBd({ id: this.siteId, @@ -624,6 +647,34 @@ export class Orbiter { schéma: OrbiterSiteDbSchema, }), ); + + const userId = await this.constellation.obtIdCompte(); + if (userId) { + const isModerator = + await this.constellation.orbite.appliquerFonctionBdOrbite({ + idBd: modDbId, + fonction: "get", + args: ["moderators", userId], + }); + + if (isModerator) { + this.events.emit("user role changed", { + userId, + role: isModerator === "MODÉRATEUR" ? "ADMIN" : "MODERATOR", + }); + } else { + await this.constellation.orbite.appliquerFonctionBdOrbite({ + idBd: modDbId, + fonction: "set", + args: ["moderators", userId, "MEMBRE"], + }); + + this.events.emit("user role changed", { + userId, + role: "MODERATOR", + }); + } + } } async orbiterConfig(): Promise<{ @@ -936,10 +987,12 @@ export class Orbiter { idBd: id, clefTableau: FEATURED_RELEASES_TABLE_KEY, f: async (featured) => { - await fSuivreBd(featured.map((x) => ({ - id: x.id, - featured: x.données, - }))) + await fSuivreBd( + featured.map((x) => ({ + id: x.id, + featured: x.données, + })), + ); }, }, ); @@ -1151,23 +1204,36 @@ export class Orbiter { async addRelease(release: Release): Promise { const { swarmId, swarmSchema } = await this.orbiterConfig(); - await this.constellation.bds.ajouterÉlémentÀTableauUnique({ - schémaBd: swarmSchema, - idNuéeUnique: swarmId, - clefTableau: RELEASES_DB_TABLE_KEY, - vals: removeUndefined(release), + const resultIds = await retryDbOperation(async () => { + return await this.constellation.bds.ajouterÉlémentÀTableauUnique({ + schémaBd: swarmSchema, + idNuéeUnique: swarmId, + clefTableau: RELEASES_DB_TABLE_KEY, + vals: removeUndefined(release), + }); }); + + if (resultIds && resultIds.length > 0) { + this.events.emit("release added", { + releaseId: resultIds[0], + release, + }); + } } async removeRelease(releaseId: string) { const { swarmId, swarmSchema } = await this.orbiterConfig(); - await this.constellation.bds.effacerÉlémentDeTableauUnique({ - schémaBd: swarmSchema, - idNuéeUnique: swarmId, - clefTableau: RELEASES_DB_TABLE_KEY, - idÉlément: releaseId, + await retryDbOperation(async () => { + await this.constellation.bds.effacerÉlémentDeTableauUnique({ + schémaBd: swarmSchema, + idNuéeUnique: swarmId, + clefTableau: RELEASES_DB_TABLE_KEY, + idÉlément: releaseId, + }); }); + + this.events.emit("release removed", { releaseId }); } async editRelease({ @@ -1191,22 +1257,33 @@ export class Orbiter { async addCollection(collection: Collection): Promise { const { swarmId, swarmSchema } = await this.orbiterConfig(); - await this.constellation.bds.ajouterÉlémentÀTableauUnique({ - schémaBd: swarmSchema, - idNuéeUnique: swarmId, - clefTableau: COLLECTIONS_DB_TABLE_KEY, - vals: removeUndefined(collection), + const resultIds = await retryDbOperation(async () => { + return await this.constellation.bds.ajouterÉlémentÀTableauUnique({ + schémaBd: swarmSchema, + idNuéeUnique: swarmId, + clefTableau: COLLECTIONS_DB_TABLE_KEY, + vals: removeUndefined(collection), + }); }); + + if (resultIds && resultIds.length > 0) { + this.events.emit("collection added", { + collectionId: resultIds[0], + collection, + }); + } } async removeCollection(collectionId: string) { const { swarmId, swarmSchema } = await this.orbiterConfig(); - await this.constellation.bds.effacerÉlémentDeTableauUnique({ - schémaBd: swarmSchema, - idNuéeUnique: swarmId, - clefTableau: COLLECTIONS_DB_TABLE_KEY, - idÉlément: collectionId, + await retryDbOperation(async () => { + await this.constellation.bds.effacerÉlémentDeTableauUnique({ + schémaBd: swarmSchema, + idNuéeUnique: swarmId, + clefTableau: COLLECTIONS_DB_TABLE_KEY, + idÉlément: collectionId, + }); }); } @@ -1303,9 +1380,14 @@ export class Orbiter { }: { image?: { contenu: Uint8Array; nomFichier: string }; }): Promise { - if (image) - return await this.constellation.profil.sauvegarderImage({ image }); - else return await this.constellation.profil.effacerImage(); + if (image) { + // Pass the image object with the correct structure + return await this.constellation.profil.sauvegarderImage({ + image + }); + } else { + return await this.constellation.profil.effacerImage(); + } } async addContactInfo({ @@ -1330,6 +1412,24 @@ export class Orbiter { }): Promise { return await this.constellation.profil.effacerContact({ type, contact }); } + /** + * Listen for Orbiter events + * @param event The event name to listen for + * @param listener The callback function to execute when the event occurs + * @returns A function to remove the event listener + */ + listenForEvents({ + event, + listener, + }: { + event: E; + listener: OrbiterEvents[E]; + }): () => void { + this.events.on(event, listener); + return () => { + this.events.off(event, listener); + }; + } // async deleteAccount(): Promise { // return await this.constellation.fermerCompte(); @@ -1381,7 +1481,7 @@ export class Orbiter { f, accountId, }: { - f: types.schémaFonctionSuivi<{ image: Uint8Array; idImage: string; } | null>; + f: types.schémaFonctionSuivi<{ image: Uint8Array; idImage: string } | null>; accountId?: string; }): Promise { return await this.constellation.profil.suivreImage({ @@ -1441,7 +1541,7 @@ export class Orbiter { cid, startTime, endTime, - promoted + promoted, }: { cid: string; startTime: string; @@ -1481,7 +1581,6 @@ export class Orbiter { }); } - async followIsModerator({ f, userId, @@ -1519,44 +1618,61 @@ export class Orbiter { const { modDbId, swarmId } = await this.orbiterConfig(); - await this.constellation.nuées.inviterAuteur({ - idNuée: swarmId, - idCompteAuteur: userId, - rôle: admin ? "MODÉRATEUR" : "MEMBRE", + await retryDbOperation(async () => { + await this.constellation.nuées.inviterAuteur({ + idNuée: swarmId, + idCompteAuteur: userId, + rôle: admin ? "MODÉRATEUR" : "MEMBRE", + }); }); - await this.constellation.bds.inviterAuteur({ - idBd: modDbId, - idCompteAuteur: userId, - rôle: admin ? "MODÉRATEUR" : "MEMBRE", + + await retryDbOperation(async () => { + await this.constellation.bds.inviterAuteur({ + idBd: modDbId, + idCompteAuteur: userId, + rôle: admin ? "MODÉRATEUR" : "MEMBRE", + }); }); + if (admin) { - await this.constellation.donnerAccès({ - idBd: this.siteId, - identité: userId, - rôle: "MODÉRATEUR", + await retryDbOperation(async () => { + await this.constellation.donnerAccès({ + idBd: this.siteId, + identité: userId, + rôle: "MODÉRATEUR", + }); }); } + + this.events.emit("user role changed", { + userId, + role: admin ? "ADMIN" : "MODERATOR", + }); } async blockRelease({ cid }: { cid: string }): Promise { const { modDbId } = await this.orbiterConfig(); - return ( - await this.constellation.bds.ajouterÉlémentÀTableauParClef({ + const result = await retryDbOperation(async () => { + return await this.constellation.bds.ajouterÉlémentÀTableauParClef({ idBd: modDbId, clefTableau: BLOCKED_RELEASES_TABLE_KEY, vals: { [BLOCKED_RELEASES_RELEASE_ID_COLUMN]: cid }, - }) - )[0]; + }); + }); + + return result[0]; } async unblockRelease({ id }: { id: string }): Promise { const { modDbId } = await this.orbiterConfig(); - await this.constellation.bds.effacerÉlémentDeTableauParClef({ - idBd: modDbId, - clefTableau: BLOCKED_RELEASES_TABLE_KEY, - idÉlément: id, + await retryDbOperation(async () => { + await this.constellation.bds.effacerÉlémentDeTableauParClef({ + idBd: modDbId, + clefTableau: BLOCKED_RELEASES_TABLE_KEY, + idÉlément: id, + }); }); } @@ -1665,20 +1781,29 @@ export class Orbiter { const vals: types.élémentsBd = { [CONTENT_CATEGORIES_CATEGORY_ID]: category.categoryId, [CONTENT_CATEGORIES_DISPLAY_NAME]: category.displayName, - [CONTENT_CATEGORIES_METADATA_SCHEMA]: JSON.stringify(category.metadataSchema), - } + [CONTENT_CATEGORIES_METADATA_SCHEMA]: JSON.stringify( + category.metadataSchema, + ), + }; if (category.featured) { - vals[CONTENT_CATEGORIES_FEATURED] = category.featured + vals[CONTENT_CATEGORIES_FEATURED] = category.featured; } - const elementIds = await this.constellation.bds.ajouterÉlémentÀTableauParClef({ - idBd: modDbId, - clefTableau: CONTENT_CATEGORIES_TABLE_KEY, - vals, - }); + const elementIds = + await this.constellation.bds.ajouterÉlémentÀTableauParClef({ + idBd: modDbId, + clefTableau: CONTENT_CATEGORIES_TABLE_KEY, + vals, + }); return elementIds[0]; } - async editCategory({ elementId, category }: { elementId: string; category: Partial }): Promise { + async editCategory({ + elementId, + category, + }: { + elementId: string; + category: Partial; + }): Promise { const { modDbId } = await this.orbiterConfig(); await this.constellation.bds.modifierÉlémentDeTableauParClef({ @@ -1705,35 +1830,49 @@ export class Orbiter { f: types.schémaFonctionSuivi; }): Promise { const { modDbId } = await this.orbiterConfig(); - - return await this.constellation.bds.suivreDonnéesDeTableauParClef({ - idBd: modDbId, - clefTableau: CONTENT_CATEGORIES_TABLE_KEY, - f: async (categories) => { - const mappedCategories = categories.map((c) => ({ - id: c.id, - contentCategory: c.données, - })); - await f(mappedCategories); + + return await this.constellation.bds.suivreDonnéesDeTableauParClef( + { + idBd: modDbId, + clefTableau: CONTENT_CATEGORIES_TABLE_KEY, + f: async (categories) => { + const mappedCategories = categories.map((c) => ({ + id: c.id, + contentCategory: c.données, + })); + await f(mappedCategories); + }, }, - }); + ); } } export const createOrbiter = async ({ constellation, + databaseConfig, }: { constellation: Constellation; + databaseConfig?: DatabaseConfig; }) => { const dir = await constellation.dossier(); + if (databaseConfig) { + const { patchConstellationConfig } = await import("./constellation-patch.js"); + patchConstellationConfig(databaseConfig); + } + const existingConfig = await getConfig({ dir, }); if (!configIsComplete(existingConfig)) { throw new Error("Configure Orbiter with `orb config` first."); } - const orbiter = new Orbiter({ constellation, ...existingConfig }); + + const orbiter = new Orbiter({ + constellation, + ...existingConfig, + ...(databaseConfig ? { database: databaseConfig } : {}), + }); return { orbiter }; }; diff --git a/src/rocksdb-adapter.ts b/src/rocksdb-adapter.ts new file mode 100644 index 0000000..e34b535 --- /dev/null +++ b/src/rocksdb-adapter.ts @@ -0,0 +1,23 @@ +import { Level } from "level"; +import { AbstractLevel } from "abstract-level"; + +interface RocksDBOptions { + valueEncoding?: string; + keyEncoding?: string; + multiProcess?: boolean; +} + +/** + * Creates a RocksDB adapter with multi-process support + * Uses the native RocksDB backend through abstract-level + */ +export function createRocksDBAdapter( + location: string, + options: RocksDBOptions = {}, +): AbstractLevel { + return new Level(location, { + ...options, + keyEncoding: options.keyEncoding || "utf8", + valueEncoding: options.valueEncoding || "json", + }) as unknown as AbstractLevel; +} diff --git a/src/types.ts b/src/types.ts index 3a5a01b..7253a93 100644 --- a/src/types.ts +++ b/src/types.ts @@ -58,9 +58,15 @@ export type VariableIds = Record<(typeof variableIdKeys)[number], string>; export type PossiblyIncompleteVariableIds = Partial; +export type DatabaseConfig = { + type: "many-level" | "rocksdb"; + multiProcess?: boolean; +}; + export type OrbiterConfig = { siteId: string; variableIds: VariableIds; + database?: DatabaseConfig; }; export type RecursivePartial = { @@ -87,6 +93,19 @@ export const possiblyIncompleteOrbiterConfigSchema: JSONSchemaType = { ) as { [P in keyof VariableIds]: { type: "string" } }, required: variableIdKeys, }, + database: { + type: "object", + nullable: true, + properties: { + type: { + type: "string", + enum: ["many-level", "rocksdb"], + }, + multiProcess: { type: "boolean", nullable: true }, + }, + required: ["type"], + }, }, required: ["siteId", "variableIds"], }; @@ -152,7 +183,9 @@ export type TrustedSite = { [TRUSTED_SITES_NAME_COL]: string; }; -export const releasesFileSchema: JSONSchemaType>[]> = { +export const releasesFileSchema: JSONSchemaType< + Release>[] +> = { type: "array", items: { type: "object", @@ -160,8 +193,7 @@ export const releasesFileSchema: JSONSchemaType> [RELEASES_NAME_COLUMN]: { type: "string" }, [RELEASES_FILE_COLUMN]: { type: "string" }, [RELEASES_AUTHOR_COLUMN]: { type: "string" }, - [RELEASES_CATEGORY_COLUMN]: { type: "string", - }, + [RELEASES_CATEGORY_COLUMN]: { type: "string" }, [RELEASES_THUMBNAIL_COLUMN]: { type: "string", nullable: true }, [RELEASES_COVER_COLUMN]: { type: "string", nullable: true }, [RELEASES_METADATA_COLUMN]: { @@ -186,18 +218,23 @@ export type ContentCategory = { [CONTENT_CATEGORIES_METADATA_SCHEMA]: T; }; -export type ContentCategoryMetadataField = Record; +export type ContentCategoryMetadataField = Record< + string, + { + type: "string" | "number" | "array"; + description: string; + options?: string[]; + } +>; export type ContentCategoryWithId = { id: string; contentCategory: ContentCategory; }; -export const categoriesFileSchema: JSONSchemaType[]> = { +export const categoriesFileSchema: JSONSchemaType< + ContentCategory[] +> = { type: "array", items: { type: "object", @@ -215,7 +252,7 @@ export const categoriesFileSchema: JSONSchemaType( + operation: () => Promise, + maxRetries: number = 5, + initialDelay: number = 100, +): Promise { + let lastError: Error | null = null; + let delay = initialDelay; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await operation(); + } catch (error: unknown) { + const err = error as Error & { + code?: string; + cause?: { code?: string }; + }; + lastError = err; + + if ( + !err.code?.includes("LOCK") && + !err.cause?.code?.includes("LOCK") && + !err.message?.includes("lock") && + !err.message?.includes("Resource temporarily unavailable") + ) { + throw error; + } + + if (attempt === maxRetries) { + throw error; + } + + console.warn( + `Database operation failed with locking error, retrying (${attempt + 1}/${maxRetries})...`, + ); + + await new Promise((resolve) => setTimeout(resolve, delay)); + delay *= 2; // Exponential backoff + } + } + + throw lastError || new Error("Failed after maximum retries"); +} diff --git a/src/version.ts b/src/version.ts index 8d7a6b7..886aae2 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ // Generated by genversion. -export const version = '0.2.34'; +export const version = '0.2.34-dev9';