Skip to content

feat: add stripTypes option to return plain objects/arrays#785

Open
podrivo wants to merge 3 commits into
gamedig:masterfrom
podrivo:feat-strip-types
Open

feat: add stripTypes option to return plain objects/arrays#785
podrivo wants to merge 3 commits into
gamedig:masterfrom
podrivo:feat-strip-types

Conversation

@podrivo

@podrivo podrivo commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Closes #657

Problem

GameDig.query() resolves to a typed Results instance whose players/bots are Players objects (an Array subclass) holding Player instances. These custom prototypes break common downstream operations:

  • structuredClone(state) / Object.assign({}, state) throw or lose data.
  • Node-RED deep-clones messages between nodes and fails with TypeError: Class constructor Players cannot be invoked without 'new'.
  • Deep-equality comparisons and some serializers treat the wrappers differently from plain data.

Today consumers must hand-roll a conversion (spreading every nested array) before they can use the result.

Fix

Add an opt-in stripTypes option (default false, so existing behavior is unchanged). When enabled, the final result is recursively converted to plain objects/arrays before being returned.

A small recursive helper toPlainObject() is exported from lib/Results.js:

export function toPlainObject (value) {
  if (Array.isArray(value)) {
    // Array.from() always yields a plain Array, even for subclasses like Players
    // (whose .map() would otherwise return another Players instance).
    return Array.from(value, toPlainObject)
  }
  if (value !== null && typeof value === 'object') {
    // Leave already clone-friendly objects untouched rather than flattening them.
    if (Buffer.isBuffer(value) || value instanceof Date) {
      return value
    }
    const out = {}
    for (const key of Object.keys(value)) {
      out[key] = toPlainObject(value[key])
    }
    return out
  }
  return value
}

It's applied at the single success-return point in QueryRunner.run():

return optionsCollection.stripTypes ? toPlainObject(response) : response

stripTypes: false was added to defaultOptions, the flag was registered for the CLI (bin/gamedig.js), and it was documented in the README options table.

Implementation notes:

  • Results has no existing toJSON, so nothing to reuse — but Players/Player already serialize fine via JSON.stringify. The helper is needed for the non-JSON cases (structuredClone, prototype checks, in-place plain data).
  • Array.from() is used instead of .map() because Players.prototype.map() returns another Players instance (Array species), which would defeat the purpose.
  • The conversion is non-mutating (the original typed object is left intact) and preserves Buffer/Date values, which are already structuredClone-friendly.

Examples

import { GameDig } from 'gamedig'

// Default: typed Results (unchanged behavior)
const typed = await GameDig.query({ type: 'csgo', host: '127.0.0.1' })
typed.players instanceof Array        // true, but it's a `Players` subclass
Object.getPrototypeOf(typed)          // Results.prototype

// Opt-in: plain objects/arrays
const plain = await GameDig.query({ type: 'csgo', host: '127.0.0.1', stripTypes: true })
Object.getPrototypeOf(plain)          // Object.prototype
Object.getPrototypeOf(plain.players)  // Array.prototype
structuredClone(plain)                // works, no throw

CLI:

gamedig --type csgo --stripTypes 127.0.0.1:27015

Node-RED (the motivating case) becomes a one-liner — no manual nested spreading:

GameDig.query({ ...options, stripTypes: true }).then((state) => {
  msg.data = state // plain objects/arrays, safe for Node-RED to clone
  node.send(msg)
})

Compatibility

  • Fully backwards-compatible: stripTypes defaults to false, so the default return value remains the typed Results instance.
  • No changes to the response schema/shape — only the prototypes of the returned objects/arrays change when the flag is enabled.
  • New named export toPlainObject from lib/Results.js (additive).

Testing

  • Built a fixture Results object (typed Players with Player entries plus a nested raw object) and asserted, with the option enabled:
    • Object.getPrototypeOf(result) === Object.prototype (and the same for players, each player, raw, and nested objects).
    • players is a true Array (not a Players subclass) and entries are not Player instances.
    • JSON.parse(JSON.stringify(result)) deep-equals the result.
    • structuredClone(result) succeeds and deep-equals the result.
    • The original typed object is left unmodified (non-mutating).
    • Buffer/Date values are preserved rather than flattened.
  • All assertions passed.
  • npm run lint:check: changes introduce 0 new lint errors. Note the repo's npm run lint:check already fails on a clean master (identical 31 pre-existing errors before and after this change), because .eslintrc.json pins ecmaVersion: 2021 while the codebase uses ES2022 class/private fields — unrelated to this PR.

podrivo added 3 commits June 20, 2026 12:31
…ainObject utility

- Added 'stripTypes' boolean option to the game query configuration in gamedig.js and QueryRunner.js.
- Implemented 'toPlainObject' function in Results.js to convert query results into plain JavaScript objects, improving serialization and comparison capabilities.
- Documented the new 'stripTypes' boolean option in the query configuration section of README.md, which allows returning results as plain JavaScript objects/arrays for improved serialization and compatibility.
- Added entry for the new `stripTypes` option that allows returning plain JavaScript objects/arrays instead of typed instances, enhancing serialization and compatibility.
@podrivo podrivo changed the title feat: strip types feat: add stripTypes option to return plain objects/arrays Jun 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: allow requesting basic object/array response instead of typed objects

1 participant