CoolFace
Apppublic

PixelPiggy/CS_float

sourceHugging Faceupdated 10mo agoView on Hugging Face
0likes
postgres.js495 linesDownload Raw Back to lib
1const { Pool } = require('pg'),2    utils = require('./utils'),3    winston = require('winston');4 5 6function flatten(arr){7    const newArr = [];8    arr.forEach(v => v.forEach(p => newArr.push(p)));9    return newArr10}11 12class Postgres {13    constructor(url, enableBulkInserts) {14        this.pool = new Pool({15            connectionString: url16        });17 18        this.enableBulkInserts = enableBulkInserts || false;19 20        if (enableBulkInserts) {21            this.queuedInserts = [];22 23            setInterval(() => {24                if (this.queuedInserts.length > 0) {25                    const copy = [...this.queuedInserts];26                    this.queuedInserts = [];27                    this.handleBulkInsert(copy);28                }29            }, 1000);30        }31    }32 33    connect() {34        return this.pool.connect().then(() => this.ensureSchema());35    }36 37    /*38        Returns the following properties in a 32 bit integer39                      rarity     quality    origin40        0000000000   00000000   00000000   0000000041        <future>      8 bits     8 bits     8 bits42     */43    static storeProperties(origin, quality, rarity) {44        return origin | (quality << 8) | (rarity << 16);45    }46 47    static extractProperties(prop) {48        return {49            origin: prop & ((1 << 8) - 1),50            quality: (prop >> 8) & ((1 << 8) - 1),51            rarity: (prop >> 16) & ((1 << 8) - 1)52        }53    }54 55    async ensureSchema() {56        await this.pool.query(`CREATE TABLE IF NOT EXISTS items (57            ms          bigint  NOT NULL,58            a           bigint  NOT NULL,59            d           bigint  NOT NULL,60            paintseed   smallint NOT NULL,61            paintwear   integer NOT NULL,62            defindex    smallint NOT NULL,63            paintindex  smallint NOT NULL,64            stattrak    boolean NOT NULL,65            souvenir    boolean NOT NULL,66            props       integer NOT NULL,67            stickers    jsonb,68            updated     timestamp NOT NULL,69            rarity      smallint NOT NULL,70            floatid     bigint  NOT NULL,71            price       integer,72            PRIMARY KEY (a)73        );74 75        CREATE TABLE IF NOT EXISTS history (76            floatid     bigint  NOT NULL,77            a           bigint  NOT NULL,78            steamid     bigint  NOT NULL,79            created_at  timestamp NOT NULL,80            price       integer,81            PRIMARY KEY (floatid, a)82        );83 84        ALTER TABLE items ADD COLUMN IF NOT EXISTS floatid BIGINT;85        ALTER TABLE items ADD COLUMN IF NOT EXISTS price INTEGER;86        ALTER TABLE items ADD COLUMN IF NOT EXISTS listed_price INTEGER;87        ALTER TABLE items ADD COLUMN IF NOT EXISTS keychains JSONB;88        ALTER TABLE history ADD COLUMN IF NOT EXISTS price INTEGER;89 90        -- Float ID is defined as the first asset id we've seen for an item91 92        CREATE OR REPLACE FUNCTION is_steamid(IN val bigint)93            RETURNS boolean94            LANGUAGE 'plpgsql'95            IMMUTABLE96            PARALLEL SAFE97        AS $BODY$BEGIN98            IF val < 76561197960265728 THEN99                RETURN FALSE;100            ELSIF (val >> 56) > 5 THEN101                RETURN FALSE;102            ELSIF ((val >> 32) & ((1 << 20) - 1)) > 32 THEN103                RETURN FALSE;104            END IF;105        106            RETURN TRUE;107        END;$BODY$;108        109        CREATE OR REPLACE FUNCTION extend_history()110        RETURNS TRIGGER111        AS $$112        BEGIN113            -- Handle cases where the floatid isn't there114            IF NEW.floatid IS NULL THEN115                NEW.floatid = OLD.a;116            END IF;117 118            IF NEW.a = OLD.a THEN119                -- Ignore handling, no new item details, updating existing120                RETURN NEW;121            END IF;122 123            IF NEW.a < OLD.a THEN124                -- If we find an older asset id than the current, still want to add it to history if not there125                INSERT INTO history VALUES (NEW.floatid, NEW.a, NEW.ms, NEW.updated, NULL) ON CONFLICT DO NOTHING;126                127                -- Prevent update to this row128                RETURN NULL;129            END IF;130 131            IF (is_steamid(OLD.ms) AND OLD.ms != NEW.ms) OR OLD.price IS NOT NULL THEN132                -- We care about history for inventory changes or market listings that had price data133                INSERT INTO history VALUES (NEW.floatid, OLD.a, OLD.ms, OLD.updated, OLD.price);134            END IF;135 136            -- Reset the price if it is the same, it is possible that the item was sold for the exact same amount in a row137            -- and we clear it here, but that isn't too much of a concern for the application of the data138            -- This ensures that outdated instances contributing to the same db don't conflict state139            IF NEW.price = OLD.price OR NEW.listed_price = OLD.listed_price OR is_steamid(NEW.ms) THEN140                NEW.price = NULL;141                NEW.listed_price = NULL;142            END IF;143 144            RETURN NEW;145        END;146        $$147        LANGUAGE 'plpgsql';148 149        DROP TRIGGER IF EXISTS extend_history_trigger150            ON items;151 152        CREATE TRIGGER extend_history_trigger153            BEFORE UPDATE ON items154            FOR EACH ROW155            EXECUTE PROCEDURE extend_history();156 157        CREATE OR REPLACE FUNCTION ensure_floatid()158        RETURNS TRIGGER159        AS $$160        BEGIN161            IF NEW.floatid IS NULL THEN162                NEW.floatid = NEW.a;163            END IF;164            165            RETURN NEW;166        END;167        $$168        LANGUAGE 'plpgsql';169 170        DROP TRIGGER IF EXISTS ensure_floatid_trigger171            ON items;172 173        CREATE TRIGGER ensure_floatid_trigger174            BEFORE INSERT ON items175            FOR EACH ROW176            EXECUTE PROCEDURE ensure_floatid();177        `);178 179        await this.pool.query(`CREATE INDEX IF NOT EXISTS i_stickers ON items USING gin (stickers jsonb_path_ops) 180                                WHERE stickers IS NOT NULL`);181        await this.pool.query(`CREATE INDEX IF NOT EXISTS i_paintwear ON items (paintwear)`);182        await this.pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS i_unique_item ON 183                                items (defindex, paintindex, paintwear, paintseed)`);184        await this.pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS i_unique_fid ON items (floatid)`);185    }186 187    async insertItemData(item, price) {188        if (this.enableBulkInserts) {189            this.queuedInserts.push([item, price]);190        } else {191            await this.handleBulkInsert([[item, price]]);192        }193    }194 195    /**196     * Bulk handler to improve insert performance with 300+ rows at once197     * @param data [[item, price]]198     * @returns {Promise<void>}199     */200    async handleBulkInsert(data) {201        const values = [];202        const uniqueItems = new Set();203 204        for (let [item, price] of data) {205            item = Object.assign({}, item);206 207            // Store float as int32 to prevent float rounding errors208            // Postgres doesn't support unsigned types, so we use signed here209            const buf = Buffer.alloc(4);210            buf.writeFloatBE(item.floatvalue, 0);211            item.paintwear = buf.readInt32BE(0);212 213            if (item.floatvalue <= 0 && item.defindex !== 507) {214                // Only insert weapons, naive check215                // Special case for the 0 float Karambit216                continue;217            }218 219            // Postgres doesn't support unsigned 64 bit ints, so we convert them to signed220            item.s = utils.unsigned64ToSigned(item.s).toString();221            item.a = utils.unsigned64ToSigned(item.a).toString();222            item.d = utils.unsigned64ToSigned(item.d).toString();223            item.m = utils.unsigned64ToSigned(item.m).toString();224 225            const stickers = item.stickers.length > 0 ? item.stickers.map((s) => {226                const res = {s: s.slot, i: s.stickerId};227                if (s.wear) {228                    res.w = s.wear;229                }230                if (s.rotation) {231                    res.r = s.rotation;232                }233                if (s.offset_x) {234                    res.x = s.offset_x;235                }236                if (s.offset_y) {237                    res.y = s.offset_y;238                }239                return res;240            }) : null;241 242            if (stickers) {243                // Add a property on stickers with duplicates that signifies how many dupes there are244                // Only add this property to one of the dupe stickers in the array245                for (const sticker of stickers) {246                    const matching = stickers.filter((s) => s.i === sticker.i);247                    if (matching.length > 1 && !matching.find((s) => s.d > 1)) {248                        sticker.d = matching.length;249                    }250                }251            }252 253            const keychains = item.keychains.length > 0 ? item.keychains.map((s) => {254                const res = {s: s.slot, i: s.sticker_id};255                if (s.wear) {256                    res.w = s.wear;257                }258                if (s.scale) {259                    res.sc = s.scale;260                }261                if (s.rotation) {262                    res.r = s.rotation;263                }264                if (s.tint_id) {265                    res.t = s.tint_id;266                }267                if (s.offset_x) {268                    res.x = s.offset_x;269                }270                if (s.offset_y) {271                    res.y = s.offset_y;272                }273                if (s.offset_z) {274                    res.z = s.offset_z;275                }276                if (s.pattern) {277                    res.p = s.pattern;278                }279                return res;280            }) : null;281 282            const ms = item.s !== '0' ? item.s : item.m;283            const isStattrak = item.killeatervalue !== null;284            const isSouvenir = item.quality === 12;285 286            const props = Postgres.storeProperties(item.origin, item.quality, item.rarity);287 288            price = price || null;289 290            // Prevent two of the same item from being inserted in the same statement (causes postgres to get angry)291            const key = `${item.defindex}_${item.paintindex}_${item.paintwear}_${item.paintseed}`;292            if (uniqueItems.has(key)) {293                continue;294            } else {295                uniqueItems.add(key);296            }297 298            values.push([ms, item.a, item.d, item.paintseed, item.paintwear, item.defindex, item.paintindex, isStattrak,299                isSouvenir, props, JSON.stringify(stickers), JSON.stringify(keychains), item.rarity, price]);300        }301 302        if (values.length === 0) {303            return;304        }305 306        try {307            const query = Postgres.buildQuery(values.length);308            await this.pool.query(query, flatten(values));309            winston.debug(`Inserted/updated ${values.length} items`)310        } catch (e) {311            winston.warn(e);312        }313    }314 315    static buildQuery(itemCount) {316        const values = [];317        let i = 1;318 319        // Builds binding pattern such as ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12::jsonb, now(), $13, NULL, $14)320        for (let c = 0; c < itemCount; c++) {321            values.push(`($${i++}, $${i++}, $${i++}, $${i++}, $${i++}, $${i++}, $${i++}, $${i++}, $${i++}, $${i++}, $${i++}::jsonb, $${i++}::jsonb, now(), $${i++}, NULL, $${i++})`);322        }323 324        return `INSERT INTO items (ms, a, d, paintseed, paintwear, defindex, paintindex, stattrak, souvenir, props, stickers, keychains, updated, rarity, floatid, price)325                VALUES ${values.join(', ')} ON CONFLICT(defindex, paintindex, paintwear, paintseed) DO UPDATE SET ms=excluded.ms, a=excluded.a, d=excluded.d, stickers=excluded.stickers, keychains=excluded.keychains, updated=now()`;326    }327 328    updateItemPrice(assetId, price) {329        return this.pool.query(`UPDATE items SET price = $1 WHERE a = $2`, [price, assetId]);330    }331 332    async getItemData(links) {333        // Chunking into db calls of 100 each is more performant334        const chunked = utils.chunkArray(links, 100);335        const promises = chunked.map(e => this._getItemData(e));336        const results = await Promise.all(promises);337 338        // Flatten results339        return results.reduce((acc, val) => acc.concat(val), []);340    }341 342    async _getItemData(links) {343        const aValues = links.map(e => utils.unsigned64ToSigned(e.getParams().a));344 345        const result = await this.pool.query(`346            SELECT *, 347               (SELECT Count(*)+1 348                FROM   (SELECT * 349                        FROM   items T 350                        WHERE  T.paintwear < S.paintwear 351                               AND T.defindex = S.defindex 352                               AND T.paintindex = S.paintindex 353                               AND T.stattrak = S.stattrak 354                               AND T.souvenir = S.souvenir 355                        ORDER  BY T.paintwear 356                        LIMIT  1000) as a) AS low_rank,357                (SELECT Count(*)+1358                FROM   (SELECT * 359                        FROM   items J 360                        WHERE  J.paintwear > S.paintwear 361                               AND J.defindex = S.defindex 362                               AND J.paintindex = S.paintindex 363                               AND J.stattrak = S.stattrak 364                               AND J.souvenir = S.souvenir 365                        ORDER  BY J.paintwear DESC366                        LIMIT  1000) as b) AS high_rank 367            FROM   items S368            WHERE  a= ANY($1::bigint[])`, [aValues]);369 370        return result.rows.map((item) => {371            delete item.updated;372 373            // Correspond to existing API, ensure we can still recreate the full item name374            if (item.stattrak) {375                item.killeatervalue = 0;376            } else {377                item.killeatervalue = null;378            }379 380            item.stickers = item.stickers || [];381            item.stickers = item.stickers.map((s) => {382                return {383                    stickerId: s.i,384                    slot: s.s,385                    wear: s.w,386                    rotation: s.r,387                    offset_x: s.x,388                    offset_y: s.y,389                }390            });391 392            item.keychains = item.keychains || [];393            item.keychains = item.keychains.map((s) => {394                return {395                    sticker_id: s.i,396                    slot: s.s,397                    wear: s.w,398                    scale: s.sc,399                    rotation: s.r,400                    tint_id: s.t,401                    offset_x: s.x,402                    offset_y: s.y,403                    offset_z: s.z,404                    pattern: s.p,405                }406            });407 408            item = Object.assign(Postgres.extractProperties(item.props), item);409 410            const buf = Buffer.alloc(4);411            buf.writeInt32BE(item.paintwear, 0);412            item.floatvalue = buf.readFloatBE(0);413 414            item.a = utils.signed64ToUnsigned(item.a).toString();415            item.d = utils.signed64ToUnsigned(item.d).toString();416            item.ms = utils.signed64ToUnsigned(item.ms).toString();417 418            if (utils.isSteamId64(item.ms)){419                item.s = item.ms;420                item.m = '0';421            } else {422                item.m = item.ms;423                item.s = '0';424            }425 426            item.high_rank = parseInt(item.high_rank);427            item.low_rank = parseInt(item.low_rank);428 429            // Delete the rank if above 1000 (we don't get ranking above that)430            if (item.high_rank === 1001) {431                delete item.high_rank;432            }433 434            if (item.low_rank === 1001) {435                delete item.low_rank;436            }437 438            delete item.souvenir;439            delete item.stattrak;440            delete item.paintwear;441            delete item.ms;442            delete item.props;443            delete item.price;444            delete item.listed_price;445            delete item.dupe_count;446 447            return item;448        });449    }450 451    getItemRank(id) {452        return this.pool.query(`SELECT (SELECT Count(*)+1453                                        FROM   (SELECT * 454                                                FROM   items T 455                                                WHERE  T.paintwear < S.paintwear 456                                                       AND T.defindex = S.defindex 457                                                       AND T.paintindex = S.paintindex 458                                                       AND T.stattrak = S.stattrak 459                                                       AND T.souvenir = S.souvenir 460                                                ORDER  BY T.paintwear 461                                                LIMIT  1000) as a) AS low_rank,462                                        (SELECT Count(*)+1 463                                        FROM   (SELECT * 464                                                FROM   items J 465                                                WHERE  J.paintwear > S.paintwear 466                                                       AND J.defindex = S.defindex 467                                                       AND J.paintindex = S.paintindex 468                                                       AND J.stattrak = S.stattrak 469                                                       AND J.souvenir = S.souvenir 470                                                ORDER  BY J.paintwear DESC471                                                LIMIT  1000) as b) AS high_rank 472                                FROM   items S473                                WHERE  a=$1`,474            [id]).then((res) => {475                if (res.rows.length > 0) {476                    const item = res.rows[0];477                    const result = {};478 479                    if (item.high_rank != 1001) {480                        result.high_rank = parseInt(item.high_rank);481                    }482                    if (item.low_rank != 1001) {483                        result.low_rank = parseInt(item.low_rank);484                    }485 486                    return result;487                } else {488                    return {};489                }490            });491    }492}493 494module.exports = Postgres;495