-
-
Notifications
You must be signed in to change notification settings - Fork 148
Description
Background
I recently ran into a particularly difficult to debug situation.
I have an API Client which uses the http2 library to queue and perform requests to an API server. Attached to this client and each request is a PromiseToken, which acts as a cancellation token class.
After processing GBs of data, I found my code would ultimately fail with Cannot assign to read only property 'waitParent' of object '#'. I searched through all of my compiled code for all interactions with promiseToken and even all calls to Object.freeze, Object.seal, Object.defineProperty, and Object.defineProperties. No interactions or calls that would affect this. I even started digging in to see if it was an issue with Node.
I eventually set up hooks for all of those static methods so that I could figure out what was modifying my PromiseToken.
It turns out what happens is I have an error handler function that is triggered in the event of an HTTP error. This sends the config params to the logger, which adds an entry to the DB. The config params include a reference to my PromiseToken, which means that sql.json(), calls deepFreeze on the token.
Thus, attempting to modify its state later is prevented.
Expected Behavior
The docs describe sql.json() as: Serializes value and binds it as a JSON string literal. The general expectation is that a serializer is a pure function and that it will not mutate the object passed to it.
As an example, imagine if JSON.stringify() mutated the object passed to it.
Possible Solutions
-
(simple) Add a warning to the documentation that this object will recursively mutate the object passed to it.
This is a good thing to do to start, but I hope that I can be persuasive in advising against having a serializer mutate the params. -
Create a deep copy of
valuebefore calling deepFreeze.
This is a viable solution, however, there is a cost to deep copy that is unnecessary considering that this can be performed synchronously (see solution 3) -
Synchronously serialize the information immediately when
sql.jsonis called
This is bound to be the most performant method and will not require freezing the object, as it is performed synchronously.
Steps to Reproduce
Here's a contrived example showing the problem:
// Get our record info
const rec = await db.one<MyDbRecord>(sql`SELECT * FROM mytable`);
// Add a log
await db.query(`INSERT INTO logs (record) VALUES(${sql.json(rec)})`);
// Update our record
rec.enabled = false; // Error
// ... logic to UPDATE mytable with rec would go here, but it won't get this farLogs
[DEBUG]: Ran Spied on Function: Object.freeze()
PromiseToken {
cancelled: false,
children: Set(0) {},
_resolveWait: undefined,
onCancel: undefined,
waitParent: undefined,
waitPromise: undefined
}
at Function.Object.<computed> (/home/ron/node/packages/core/dist/nld/bootstrap.js:83:35)
at deepFreeze (/home/ron/node/node_modules/slonik/dist/utilities/deepFreeze.js:24:10)
at deepFreeze (/home/ron/node/node_modules/slonik/dist/utilities/deepFreeze.js:28:7)
at deepFreeze (/home/ron/node/node_modules/slonik/dist/utilities/deepFreeze.js:28:7)
at deepFreeze (/home/ron/node/node_modules/slonik/dist/utilities/deepFreeze.js:28:7)
at deepFreeze (/home/ron/node/node_modules/slonik/dist/utilities/deepFreeze.js:28:7)
at deepFreeze (/home/ron/node/node_modules/slonik/dist/utilities/deepFreeze.js:28:7)
at Function.sql.json (/home/ron/node/node_modules/slonik/dist/factories/createSqlTag.js:98:38)
at /home/ron/node/packages/core/dist/components/logger/logger.js:138:90
at Logger.logWorker (/home/ron/node/packages/core/dist/components/logger/logger.js:152:15)
at /home/ron/node/packages/core/dist/components/logger/logger.js:68:64
at APIClient.errorHandler (/home/ron/node/packages/core/dist/components/api/api-client.js:137:50)
at runMicrotasks (<anonymous>)
at processTicksAndRejections (internal/process/task_queues.js:93:5)
at async AsyncTask.action (/home/ron/node/packages/core/dist/components/api/api-client.js:104:17)