diff --git a/@sammo/api_def/package.json b/@sammo/api_def/package.json index ee3ef5d..cea9f64 100644 --- a/@sammo/api_def/package.json +++ b/@sammo/api_def/package.json @@ -13,7 +13,7 @@ }, "author": "", "type": "module", - "license": "ISC", + "license": "MIT", "dependencies": { "@sammo/crypto": "workspace:^", "@sammo/secure_token": "workspace:^", diff --git a/@sammo/crypto/package.json b/@sammo/crypto/package.json index b6e7fa3..69bda7b 100644 --- a/@sammo/crypto/package.json +++ b/@sammo/crypto/package.json @@ -20,7 +20,7 @@ }, "author": "", "type": "module", - "license": "ISC", + "license": "MIT", "dependencies": { "@sammo/util": "workspace:^" }, diff --git a/@sammo/crypto/src/index.ts b/@sammo/crypto/src/index.ts index 0820851..7e47e2d 100644 --- a/@sammo/crypto/src/index.ts +++ b/@sammo/crypto/src/index.ts @@ -1,28 +1,4 @@ -export class NotYetImplemented extends Error { - public override name = 'NotYetImplemented'; - override toString(): string { - if (this.message) { - return this.name + ': ' + this.message; - } - else { - return this.name; - } - - } -} - - -export class InvalidArgument extends Error { - public override name = 'InvalidArgument'; - override toString(): string { - if (this.message) { - return this.name + ': ' + this.message; - } - else { - return this.name; - } - } -} +import { InvalidArgument } from '@sammo/util'; export class InvalidVType extends InvalidArgument { public override name = 'InvalidVType'; diff --git a/@sammo/gateway/package.json b/@sammo/gateway/package.json new file mode 100644 index 0000000..94b6e16 --- /dev/null +++ b/@sammo/gateway/package.json @@ -0,0 +1,23 @@ +{ + "name": "@sammo/gateway", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "scripts": { + "build": "tsc --build" + }, + "author": "", + "type": "module", + "license": "MIT", + "dependencies": { + "@sammo/api_def": "workspace:^", + "@sammo/server_util": "workspace:^", + "@sammo/util": "workspace:^", + "@strpc/express": "workspace:^", + "dotenv": "^16.3.1", + "mongoose": "^7.4.3" + }, + "devDependencies": { + "@types/node": "^20.6.3" + } +} diff --git a/@sammo/gateway/src/MongoSequence.ts b/@sammo/gateway/src/MongoSequence.ts new file mode 100644 index 0000000..008d4a0 --- /dev/null +++ b/@sammo/gateway/src/MongoSequence.ts @@ -0,0 +1,25 @@ +import type { StateIncrementer } from '@sammo/server_util'; +import SchemaSequence from './schema/SchemaSequence.js'; +import { InvalidArgument } from '@sammo/util'; + +export function MongoSequenceFactory(collectionName: string): StateIncrementer{ + + return async (increase: number) => { + if(increase <= 0){ + throw new InvalidArgument('increase must be > 0'); + } + increase = Math.ceil(increase); + + const result = await SchemaSequence.findOneAndUpdate({ + collectionName, + }, { + $inc: { + nextSeq: increase, + }, + }, { + upsert: true, + new: true, + }); + return result.nextSeq; + } +} \ No newline at end of file diff --git a/@sammo/gateway/src/api/index.ts b/@sammo/gateway/src/api/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/@sammo/gateway/src/connectDB.ts b/@sammo/gateway/src/connectDB.ts new file mode 100644 index 0000000..499cbe2 --- /dev/null +++ b/@sammo/gateway/src/connectDB.ts @@ -0,0 +1,22 @@ +import './dotenv.js'; +import { connect, Mongoose } from "mongoose"; +import { unwrap } from '@sammo/util'; + +const dbConfig = { + host: unwrap(process.env.GATEWAY_DB_HOST), + port: Number(unwrap(process.env.GATEWAY_DB_PORT)), + user: unwrap(process.env.GATEWAY_DB_USER), + password: unwrap(process.env.GATEWAY_DB_PASSWORD), + database: unwrap(process.env.GATEWAY_DB_DATABASE), +} + +const db: Promise = (async () => { + return await connect(`mongodb://${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`, { + auth:{ + username: dbConfig.user, + password: dbConfig.password, + } + }); +})(); + +export default db; \ No newline at end of file diff --git a/@sammo/gateway/src/constPath.ts b/@sammo/gateway/src/constPath.ts new file mode 100644 index 0000000..f00e15f --- /dev/null +++ b/@sammo/gateway/src/constPath.ts @@ -0,0 +1,3 @@ +import { resolve } from "node:path"; + +export const rootPath = resolve(resolve(), '../..'); \ No newline at end of file diff --git a/@sammo/gateway/src/dotenv.ts b/@sammo/gateway/src/dotenv.ts new file mode 100644 index 0000000..f9ce3ca --- /dev/null +++ b/@sammo/gateway/src/dotenv.ts @@ -0,0 +1,47 @@ +import dotenv from 'dotenv'; +import { rootPath } from './constPath.js'; +import { unwrap } from '@sammo/util'; + +let init = false; +const dirPath = rootPath +console.log(rootPath); + +function initDotEnv() { + if (process.env['NODE_ENV'] == 'production') { + dotenv.config({ path: `${dirPath}/.env.production.local` }); + dotenv.config({ path: `${dirPath}/.env.local` }); + dotenv.config({ path: `${dirPath}/.env.production` }); + } + else { + dotenv.config({ path: `${dirPath}/.env.development.local` }); + dotenv.config({ path: `${dirPath}/.env.local` }); + dotenv.config({ path: `${dirPath}/.env.development` }); + } + dotenv.config(); + init = true; +} + +if (!init) { + initDotEnv(); +} + +let _ownConfig: ReturnType|undefined = undefined; + +function generateConfig() { + return { + port: parseInt(process.env['SERVER_PORT'] ?? "3001"), + sessionSecret: unwrap(process.env['SESSION_SECRET']), + apiRootPath: process.env['API_ROOT_PATH'] ?? '/api/gateway', + } as const; +} + +export function ownConfig() { + if(_ownConfig !== undefined){ + return _ownConfig; + } + if(!init){ + initDotEnv(); + } + _ownConfig = generateConfig(); + return _ownConfig; +} diff --git a/@sammo/gateway/src/gatewayConfig.ts b/@sammo/gateway/src/gatewayConfig.ts new file mode 100644 index 0000000..08c69f7 --- /dev/null +++ b/@sammo/gateway/src/gatewayConfig.ts @@ -0,0 +1,7 @@ +import 'dotenv/config'; +import { unwrap } from "@sammo/util"; + +export const gatewayConfig = { + sessionSecret: unwrap(process.env.GATEWAY_SESSION_SECRET), + port: Number(unwrap(process.env.GATEWAY_PORT)), +}; \ No newline at end of file diff --git a/@sammo/gateway/src/index.ts b/@sammo/gateway/src/index.ts new file mode 100644 index 0000000..0fe4108 --- /dev/null +++ b/@sammo/gateway/src/index.ts @@ -0,0 +1,34 @@ +import './dotenv.js' +import 'reflect-metadata'; +import gatewayDB from './connectDB.js'; +import express, { type Request, type Response } from "express" +import session from "express-session"; +import { buildAPISystem } from '@strpc/express/generator'; +//import { sammoGatewayAPI } from './api/index.js'; +import { unwrap } from '@sammo/util';; +import { gatewayConfig } from './gatewayConfig.js'; + + + + +gatewayDB.then(async (gatewayDB) => { + // create express app + const app = express() + app.use(express.json()); + app.use(session({ + secret: unwrap(gatewayConfig.sessionSecret), + resave: false, + saveUninitialized: false, + })) + //app.set('etag', false); + + //app.use('/gateway_api', buildAPISystem(sammoGatewayAPI)); + + // start express server + app.listen(gatewayConfig.port) + + console.log(`Gateway server has started on port ${gatewayConfig.port}`) + +}).catch(error => console.log(error)); + +export default {}; \ No newline at end of file diff --git a/@sammo/gateway/src/schema/SchemaSequence.ts b/@sammo/gateway/src/schema/SchemaSequence.ts new file mode 100644 index 0000000..2c6e2e2 --- /dev/null +++ b/@sammo/gateway/src/schema/SchemaSequence.ts @@ -0,0 +1,23 @@ +import { Schema, model, Types } from 'mongoose'; + +interface ISchemaSequence { + _id: Types.ObjectId; + + collectionName: string; + nextSeq: number; +} + +export const SchemaSequence = new Schema({ + collectionName: { type: String, required: true }, + nextSeq: { + type: Number, required: true, + get: (v: number) => Math.ceil(v), + set: (v: number) => Math.ceil(v), + default: 0, + }, + +}, { autoIndex: true, autoCreate: true }) + .index({ collectionName: 1 }, { unique: true }) + ; + +export default model('SchemaSequence', SchemaSequence); \ No newline at end of file diff --git a/@sammo/gateway/tsconfig.json b/@sammo/gateway/tsconfig.json new file mode 100644 index 0000000..ebff90d --- /dev/null +++ b/@sammo/gateway/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + }, + "references": [ + { + "path": "../../@strpc/express" + }, + { + "path": "../util" + }, + { + "path": "../crypto" + }, + { + "path": "../server_util" + }, + ] +} diff --git a/@sammo/secure_token/package.json b/@sammo/secure_token/package.json index bd74e96..2137c71 100644 --- a/@sammo/secure_token/package.json +++ b/@sammo/secure_token/package.json @@ -8,7 +8,7 @@ }, "author": "", "type": "module", - "license": "ISC", + "license": "MIT", "dependencies": { "@sammo/crypto": "workspace:^", "@sammo/util": "workspace:^" diff --git a/@sammo/server_util/package.json b/@sammo/server_util/package.json new file mode 100644 index 0000000..74cb0c0 --- /dev/null +++ b/@sammo/server_util/package.json @@ -0,0 +1,23 @@ +{ + "name": "@sammo/server_util", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "scripts": { + "build": "tsc --build" + }, + "type": "module", + "keywords": [], + "author": "", + "license": "MIT", + "dependencies": { + "@sammo/api_def": "workspace:^", + "@sammo/util": "workspace:^" + }, + "devDependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "lodash-es": "^4.17.21" + } +} \ No newline at end of file diff --git a/@sammo/server_util/src/UniqueNumberAllocator.ts b/@sammo/server_util/src/UniqueNumberAllocator.ts new file mode 100644 index 0000000..d1c1165 --- /dev/null +++ b/@sammo/server_util/src/UniqueNumberAllocator.ts @@ -0,0 +1,198 @@ +export interface StateIncrementer { + (increase: number): Promise; +} + +export type NumberGroup = { + start: number; + remain: number; +} + +const AllocatorGroupBucketSize = 100; +const AllocatorPreserveThreshold = 50; + +export function* numberGenerator(numberGroups: NumberGroup[]): Generator { + for (const numberGroup of numberGroups) { + for (let i = 0; i < numberGroup.remain; i++) { + yield numberGroup.start + i; + } + } + return null; +} + +export class UniqueNumberAllocator { + private preservedNumbers: NumberGroup[]; + private incrementer: StateIncrementer; + private totalRemain: number; + private promisedCnt: number; + private promise: Promise; + + public constructor(incrementer: StateIncrementer, initialState?: NumberGroup) { + this.incrementer = incrementer; + if (!initialState) { + this.preservedNumbers = []; + this.totalRemain = 0; + } + else { + if (initialState.remain < 1) { + throw new Error('initialState.remain must be greater than 0'); + } + this.preservedNumbers = [initialState]; + this.totalRemain = initialState.remain; + } + this.promisedCnt = 0; + this.promise = Promise.resolve(); + this.preserveNext(); + } + + public static async generate(incrementer: StateIncrementer): Promise { + const initialStateEnd = await incrementer(AllocatorGroupBucketSize); + const initialState: NumberGroup = { + start: initialStateEnd - AllocatorGroupBucketSize + 1, + remain: AllocatorGroupBucketSize, + } + return new UniqueNumberAllocator(incrementer, initialState); + } + + private preserveNext(): void { + if (this.promisedCnt + this.totalRemain >= AllocatorGroupBucketSize) { + return; + } + + if (this.totalRemain >= AllocatorPreserveThreshold) { + return; + } + + const waiter = this.promise; + + this.promise = (async () => { + this.promisedCnt += AllocatorGroupBucketSize; + const nextP = this.incrementer(AllocatorGroupBucketSize); + await waiter; + const next = await nextP; + if (this.preservedNumbers.length > 0) { + const last = this.preservedNumbers[this.preservedNumbers.length - 1]; + if (last.start + last.remain === next - AllocatorGroupBucketSize + 1) { + // 마지막 그룹과 연속됨 + last.remain += AllocatorGroupBucketSize; + this.totalRemain += AllocatorGroupBucketSize; + this.promisedCnt -= AllocatorGroupBucketSize; + return; + } + } + const nextGroup: NumberGroup = { + start: next - AllocatorGroupBucketSize + 1, + remain: AllocatorGroupBucketSize, + } + this.preservedNumbers.push(nextGroup); + this.totalRemain += AllocatorGroupBucketSize; + this.promisedCnt -= AllocatorGroupBucketSize; + })(); + } + + public async allocateOne(): Promise { + if (this.totalRemain > 0) { + const head = this.preservedNumbers[0]; + if (head.remain > 1) { + const next = head.start; + head.start++; + head.remain--; + this.totalRemain--; + this.preserveNext(); + return next; + } + else { + this.preservedNumbers.shift(); + this.totalRemain--; + this.preserveNext(); + return head.start; + } + } + + const next = (await this.allocate(1)).next().value; + if (next === null) { + throw new Error('incrementer failed to allocate enough numbers'); + } + return next; + } + + public async allocate(n: number): Promise> { + n = Math.ceil(n); + if (n < 1) { + throw new Error('n must be greater than 0'); + } + + if (n > this.totalRemain + this.promisedCnt) { + const endP = this.incrementer(n); + this.preserveNext(); + const end = await endP; + + return numberGenerator([{ + start: end - n + 1, + remain: n, + }]); + } + + const result: NumberGroup[] = []; + + if (this.totalRemain > 0) { + const head = this.preservedNumbers[0]; + const headRemain = head.remain; + + if (n < headRemain) { + result.push({ + start: head.start, + remain: n, + }); + head.start += n; + head.remain -= n; + this.totalRemain -= n; + this.preserveNext(); + return numberGenerator(result); + } + if (n === headRemain) { + this.preservedNumbers.shift(); + result.push(head); + this.totalRemain -= n; + this.preserveNext(); + return numberGenerator(result); + } + + } + + //assert n <= this.totalRemain + this.promisedRemain + + if (n > this.totalRemain) { + await this.promise; + if (n > this.totalRemain) { + throw new Error('incrementer failed to allocate enough numbers'); + } + } + + this.totalRemain -= n; + this.preserveNext(); + + let remain = n; + let idx = 0; + while (remain > 0) { + const head = this.preservedNumbers[idx]; + if (head.remain <= remain) { + remain -= head.remain; + result.push(head); + idx++; + continue; + } + + result.push({ + start: head.start, + remain: remain, + }); + head.start += remain; + head.remain -= remain; + remain = 0; + break; + } + this.preservedNumbers = this.preservedNumbers.slice(idx); + + return numberGenerator(result); + } +} \ No newline at end of file diff --git a/@sammo/server_util/src/index.ts b/@sammo/server_util/src/index.ts new file mode 100644 index 0000000..aaf34aa --- /dev/null +++ b/@sammo/server_util/src/index.ts @@ -0,0 +1 @@ +export { UniqueNumberAllocator, type StateIncrementer } from './UniqueNumberAllocator.js'; \ No newline at end of file diff --git a/@sammo/server_util/tsconfig.json b/@sammo/server_util/tsconfig.json new file mode 100644 index 0000000..b70d41f --- /dev/null +++ b/@sammo/server_util/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "node16", + "moduleResolution": "node16", + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "importHelpers": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "skipLibCheck": true, + "incremental": true, + "rootDir": "./src", + "outDir": "./dist", + "composite": true + }, + "references": [ + { + "path": "../util" + }, + { + "path": "../crypto" + }, + ] +} diff --git a/@sammo/util/package.json b/@sammo/util/package.json index 9ddd6f4..f3794f8 100644 --- a/@sammo/util/package.json +++ b/@sammo/util/package.json @@ -9,7 +9,7 @@ "type": "module", "keywords": [], "author": "", - "license": "ISC", + "license": "MIT", "devDependencies": { "lodash-es": "^4.17.21" }, diff --git a/@sammo/util/src/error.ts b/@sammo/util/src/error.ts index 58b688e..a445b98 100644 --- a/@sammo/util/src/error.ts +++ b/@sammo/util/src/error.ts @@ -15,4 +15,29 @@ export class RuntimeError extends Error { return this.name; } } -} \ No newline at end of file +} + +export class NotYetImplemented extends Error { + public override name = 'NotYetImplemented'; + override toString(): string { + if (this.message) { + return this.name + ': ' + this.message; + } + else { + return this.name; + } + + } +} + +export class InvalidArgument extends Error { + public override name = 'InvalidArgument'; + override toString(): string { + if (this.message) { + return this.name + ': ' + this.message; + } + else { + return this.name; + } + } +} diff --git a/@strpc/client_ky/package.json b/@strpc/client_ky/package.json index edec5fa..98379e5 100644 --- a/@strpc/client_ky/package.json +++ b/@strpc/client_ky/package.json @@ -19,7 +19,7 @@ }, "type": "module", "author": "", - "license": "ISC", + "license": "MIT", "peerDependencies": { "ky": "^1.0.1", "lodash-es": "^4.17.21" diff --git a/@strpc/def/package.json b/@strpc/def/package.json index d41e15e..95bd123 100644 --- a/@strpc/def/package.json +++ b/@strpc/def/package.json @@ -13,5 +13,5 @@ }, "type": "module", "author": "", - "license": "ISC" + "license": "MIT" } diff --git a/@strpc/express/package.json b/@strpc/express/package.json index b0d690a..9f15d00 100644 --- a/@strpc/express/package.json +++ b/@strpc/express/package.json @@ -13,7 +13,7 @@ }, "type": "module", "author": "", - "license": "ISC", + "license": "MIT", "dependencies": { "@strpc/def": "workspace:^" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6f9853..b7f8e9d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -210,6 +210,31 @@ importers: specifier: ^5.2.2 version: 5.2.2 + '@sammo/gateway': + dependencies: + '@sammo/api_def': + specifier: workspace:^ + version: link:../api_def + '@sammo/server_util': + specifier: workspace:^ + version: link:../server_util + '@sammo/util': + specifier: workspace:^ + version: link:../util + '@strpc/express': + specifier: workspace:^ + version: link:../../@strpc/express + dotenv: + specifier: ^16.3.1 + version: 16.3.1 + mongoose: + specifier: ^7.4.3 + version: 7.4.3 + devDependencies: + '@types/node': + specifier: ^20.6.3 + version: 20.6.3 + '@sammo/secure_token': dependencies: '@sammo/crypto': @@ -229,6 +254,19 @@ importers: specifier: ^3.22.2 version: 3.22.2 + '@sammo/server_util': + dependencies: + '@sammo/api_def': + specifier: workspace:^ + version: link:../api_def + '@sammo/util': + specifier: workspace:^ + version: link:../util + devDependencies: + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + '@sammo/util': devDependencies: lodash-es: @@ -1380,7 +1418,7 @@ packages: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: '@types/connect': 3.4.35 - '@types/node': 18.17.0 + '@types/node': 20.6.3 dev: true /@types/bootstrap@5.2.6: @@ -1392,13 +1430,13 @@ packages: /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 18.17.0 + '@types/node': 20.6.3 dev: true /@types/express-serve-static-core@4.17.35: resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==} dependencies: - '@types/node': 18.17.0 + '@types/node': 20.6.3 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 '@types/send': 0.17.1 @@ -1486,10 +1524,10 @@ packages: /@types/node@18.17.0: resolution: {integrity: sha512-GXZxEtOxYGFchyUzxvKI14iff9KZ2DI+A6a37o6EQevtg6uO9t+aUZKcaC1Te5Ng1OnLM7K9NVVj+FbecD9cJg==} + dev: true /@types/node@20.6.3: resolution: {integrity: sha512-HksnYH4Ljr4VQgEy2lTStbCKv/P590tmPe5HqOnv9Gprffgv5WXAY+Y5Gqniu0GGqeTCUdBnzC3QSrzPkBkAMA==} - dev: true /@types/qs@6.9.7: resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} @@ -1507,7 +1545,7 @@ packages: resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==} dependencies: '@types/mime': 1.3.2 - '@types/node': 18.17.0 + '@types/node': 20.6.3 dev: true /@types/serve-static@1.15.2: @@ -1515,7 +1553,7 @@ packages: dependencies: '@types/http-errors': 2.0.1 '@types/mime': 3.0.1 - '@types/node': 18.17.0 + '@types/node': 20.6.3 dev: true /@types/stack-utils@2.0.1: @@ -1533,7 +1571,7 @@ packages: /@types/whatwg-url@8.2.2: resolution: {integrity: sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==} dependencies: - '@types/node': 18.17.0 + '@types/node': 20.6.3 '@types/webidl-conversions': 7.0.0 dev: false