@btc-vision/bitcoin 6.5.5 → 7.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUDIT/README.md +9 -0
- package/HOW_TO_WRITE_GOOD_CODE.md +2436 -0
- package/SECURITY.md +27 -0
- package/benchmark/psbt-2000-inputs.bench.ts +178 -0
- package/benchmark/signing.bench.ts +147 -0
- package/browser/address.d.ts +56 -9
- package/browser/address.d.ts.map +1 -0
- package/browser/bech32utils.d.ts +9 -1
- package/browser/bech32utils.d.ts.map +1 -0
- package/browser/bip66.d.ts +11 -6
- package/browser/bip66.d.ts.map +1 -0
- package/browser/block.d.ts +117 -11
- package/browser/block.d.ts.map +1 -0
- package/browser/branded.d.ts +20 -0
- package/browser/branded.d.ts.map +1 -0
- package/browser/crypto/crypto.d.ts +1 -0
- package/browser/crypto/crypto.d.ts.map +1 -0
- package/browser/crypto.d.ts +46 -7
- package/browser/crypto.d.ts.map +1 -0
- package/browser/ecc/context.d.ts +129 -0
- package/browser/ecc/context.d.ts.map +1 -0
- package/browser/ecc/index.d.ts +11 -0
- package/browser/ecc/index.d.ts.map +1 -0
- package/browser/ecc/types.d.ts +128 -0
- package/browser/ecc/types.d.ts.map +1 -0
- package/browser/ecpair.d.ts +99 -0
- package/browser/errors.d.ts +124 -0
- package/browser/errors.d.ts.map +1 -0
- package/browser/index.d.ts +32 -5
- package/browser/index.d.ts.map +1 -0
- package/browser/index.js +12482 -101
- package/browser/io/BinaryReader.d.ts +276 -0
- package/browser/io/BinaryReader.d.ts.map +1 -0
- package/browser/io/BinaryWriter.d.ts +391 -0
- package/browser/io/BinaryWriter.d.ts.map +1 -0
- package/browser/io/MemoryPool.d.ts +220 -0
- package/browser/io/MemoryPool.d.ts.map +1 -0
- package/browser/io/base64.d.ts +13 -0
- package/browser/io/base64.d.ts.map +1 -0
- package/browser/io/hex.d.ts +67 -0
- package/browser/io/hex.d.ts.map +1 -0
- package/browser/io/index.d.ts +17 -0
- package/browser/io/index.d.ts.map +1 -0
- package/browser/io/utils.d.ts +199 -0
- package/browser/io/utils.d.ts.map +1 -0
- package/browser/merkle.d.ts +10 -1
- package/browser/merkle.d.ts.map +1 -0
- package/browser/networks.d.ts +70 -9
- package/browser/networks.d.ts.map +1 -0
- package/browser/opcodes.d.ts +1 -0
- package/browser/opcodes.d.ts.map +1 -0
- package/browser/payments/bip341.d.ts +35 -9
- package/browser/payments/bip341.d.ts.map +1 -0
- package/browser/payments/embed.d.ts +112 -1
- package/browser/payments/embed.d.ts.map +1 -0
- package/browser/payments/index.d.ts +17 -10
- package/browser/payments/index.d.ts.map +1 -0
- package/browser/payments/p2ms.d.ts +150 -0
- package/browser/payments/p2ms.d.ts.map +1 -0
- package/browser/payments/p2op.d.ts +150 -24
- package/browser/payments/p2op.d.ts.map +1 -0
- package/browser/payments/p2pk.d.ts +154 -1
- package/browser/payments/p2pk.d.ts.map +1 -0
- package/browser/payments/p2pkh.d.ts +176 -1
- package/browser/payments/p2pkh.d.ts.map +1 -0
- package/browser/payments/p2sh.d.ts +150 -1
- package/browser/payments/p2sh.d.ts.map +1 -0
- package/browser/payments/p2tr.d.ts +185 -1
- package/browser/payments/p2tr.d.ts.map +1 -0
- package/browser/payments/p2wpkh.d.ts +161 -1
- package/browser/payments/p2wpkh.d.ts.map +1 -0
- package/browser/payments/p2wsh.d.ts +146 -1
- package/browser/payments/p2wsh.d.ts.map +1 -0
- package/browser/payments/types.d.ts +94 -64
- package/browser/payments/types.d.ts.map +1 -0
- package/browser/psbt/bip371.d.ts +34 -8
- package/browser/psbt/bip371.d.ts.map +1 -0
- package/browser/psbt/psbtutils.d.ts +56 -16
- package/browser/psbt/psbtutils.d.ts.map +1 -0
- package/browser/psbt/types.d.ts +245 -0
- package/browser/psbt/types.d.ts.map +1 -0
- package/browser/psbt/utils.d.ts +64 -0
- package/browser/psbt/utils.d.ts.map +1 -0
- package/browser/psbt/validation.d.ts +84 -0
- package/browser/psbt/validation.d.ts.map +1 -0
- package/browser/psbt.d.ts +82 -118
- package/browser/psbt.d.ts.map +1 -0
- package/browser/pubkey.d.ts +27 -6
- package/browser/pubkey.d.ts.map +1 -0
- package/browser/push_data.d.ts +24 -2
- package/browser/push_data.d.ts.map +1 -0
- package/browser/script.d.ts +33 -8
- package/browser/script.d.ts.map +1 -0
- package/browser/script_number.d.ts +17 -0
- package/browser/script_number.d.ts.map +1 -0
- package/browser/script_signature.d.ts +23 -5
- package/browser/script_signature.d.ts.map +1 -0
- package/browser/transaction.d.ts +160 -18
- package/browser/transaction.d.ts.map +1 -0
- package/browser/types.d.ts +36 -38
- package/browser/types.d.ts.map +1 -0
- package/browser/workers/WorkerSigningPool.d.ts +143 -0
- package/browser/workers/WorkerSigningPool.d.ts.map +1 -0
- package/browser/workers/WorkerSigningPool.node.d.ts +116 -0
- package/browser/workers/WorkerSigningPool.node.d.ts.map +1 -0
- package/browser/workers/ecc-bundle.d.ts +25 -0
- package/browser/workers/ecc-bundle.d.ts.map +1 -0
- package/browser/workers/index.d.ts +91 -0
- package/browser/workers/index.d.ts.map +1 -0
- package/browser/workers/psbt-parallel.d.ts +88 -0
- package/browser/workers/psbt-parallel.d.ts.map +1 -0
- package/browser/workers/signing-worker.d.ts +37 -0
- package/browser/workers/signing-worker.d.ts.map +1 -0
- package/browser/workers/types.d.ts +365 -0
- package/browser/workers/types.d.ts.map +1 -0
- package/build/address.d.ts +57 -10
- package/build/address.d.ts.map +1 -0
- package/build/address.js +80 -24
- package/build/address.js.map +1 -0
- package/build/bech32utils.d.ts +9 -1
- package/build/bech32utils.d.ts.map +1 -0
- package/build/bech32utils.js +10 -2
- package/build/bech32utils.js.map +1 -0
- package/build/bip66.d.ts +11 -6
- package/build/bip66.d.ts.map +1 -0
- package/build/bip66.js +32 -3
- package/build/bip66.js.map +1 -0
- package/build/block.d.ts +117 -11
- package/build/block.d.ts.map +1 -0
- package/build/block.js +204 -72
- package/build/block.js.map +1 -0
- package/build/branded.d.ts +20 -0
- package/build/branded.d.ts.map +1 -0
- package/build/branded.js +7 -0
- package/build/branded.js.map +1 -0
- package/build/crypto/crypto.d.ts +1 -0
- package/build/crypto/crypto.d.ts.map +1 -0
- package/build/crypto/crypto.js +1 -0
- package/build/crypto/crypto.js.map +1 -0
- package/build/crypto.d.ts +46 -7
- package/build/crypto.d.ts.map +1 -0
- package/build/crypto.js +65 -20
- package/build/crypto.js.map +1 -0
- package/build/ecc/context.d.ts +135 -0
- package/build/ecc/context.d.ts.map +1 -0
- package/build/ecc/context.js +232 -0
- package/build/ecc/context.js.map +1 -0
- package/build/ecc/index.d.ts +11 -0
- package/build/ecc/index.d.ts.map +1 -0
- package/build/ecc/index.js +11 -0
- package/build/ecc/index.js.map +1 -0
- package/build/ecc/types.d.ts +134 -0
- package/build/ecc/types.d.ts.map +1 -0
- package/build/ecc/types.js +8 -0
- package/build/ecc/types.js.map +1 -0
- package/build/errors.d.ts +124 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +155 -0
- package/build/errors.js.map +1 -0
- package/build/index.d.ts +32 -5
- package/build/index.d.ts.map +1 -0
- package/build/index.js +26 -3
- package/build/index.js.map +1 -0
- package/build/io/BinaryReader.d.ts +276 -0
- package/build/io/BinaryReader.d.ts.map +1 -0
- package/build/io/BinaryReader.js +425 -0
- package/build/io/BinaryReader.js.map +1 -0
- package/build/io/BinaryWriter.d.ts +391 -0
- package/build/io/BinaryWriter.d.ts.map +1 -0
- package/build/io/BinaryWriter.js +611 -0
- package/build/io/BinaryWriter.js.map +1 -0
- package/build/io/MemoryPool.d.ts +220 -0
- package/build/io/MemoryPool.d.ts.map +1 -0
- package/build/io/MemoryPool.js +309 -0
- package/build/io/MemoryPool.js.map +1 -0
- package/build/io/base64.d.ts +13 -0
- package/build/io/base64.d.ts.map +1 -0
- package/build/io/base64.js +20 -0
- package/build/io/base64.js.map +1 -0
- package/build/io/hex.d.ts +67 -0
- package/build/io/hex.d.ts.map +1 -0
- package/build/io/hex.js +138 -0
- package/build/io/hex.js.map +1 -0
- package/build/io/index.d.ts +17 -0
- package/build/io/index.d.ts.map +1 -0
- package/build/io/index.js +23 -0
- package/build/io/index.js.map +1 -0
- package/build/io/utils.d.ts +199 -0
- package/build/io/utils.d.ts.map +1 -0
- package/build/io/utils.js +271 -0
- package/build/io/utils.js.map +1 -0
- package/build/merkle.d.ts +10 -1
- package/build/merkle.d.ts.map +1 -0
- package/build/merkle.js +12 -1
- package/build/merkle.js.map +1 -0
- package/build/networks.d.ts +70 -9
- package/build/networks.d.ts.map +1 -0
- package/build/networks.js +90 -4
- package/build/networks.js.map +1 -0
- package/build/opcodes.d.ts +1 -0
- package/build/opcodes.d.ts.map +1 -0
- package/build/opcodes.js +1 -0
- package/build/opcodes.js.map +1 -0
- package/build/payments/bip341.d.ts +36 -9
- package/build/payments/bip341.d.ts.map +1 -0
- package/build/payments/bip341.js +35 -15
- package/build/payments/bip341.js.map +1 -0
- package/build/payments/embed.d.ts +120 -1
- package/build/payments/embed.d.ts.map +1 -0
- package/build/payments/embed.js +215 -34
- package/build/payments/embed.js.map +1 -0
- package/build/payments/index.d.ts +17 -10
- package/build/payments/index.d.ts.map +1 -0
- package/build/payments/index.js +20 -10
- package/build/payments/index.js.map +1 -0
- package/build/payments/p2ms.d.ts +159 -1
- package/build/payments/p2ms.d.ts.map +1 -0
- package/build/payments/p2ms.js +427 -108
- package/build/payments/p2ms.js.map +1 -0
- package/build/payments/p2op.d.ts +158 -24
- package/build/payments/p2op.d.ts.map +1 -0
- package/build/payments/p2op.js +379 -93
- package/build/payments/p2op.js.map +1 -0
- package/build/payments/p2pk.d.ts +162 -1
- package/build/payments/p2pk.d.ts.map +1 -0
- package/build/payments/p2pk.js +327 -58
- package/build/payments/p2pk.js.map +1 -0
- package/build/payments/p2pkh.d.ts +185 -1
- package/build/payments/p2pkh.d.ts.map +1 -0
- package/build/payments/p2pkh.js +467 -114
- package/build/payments/p2pkh.js.map +1 -0
- package/build/payments/p2sh.d.ts +159 -1
- package/build/payments/p2sh.d.ts.map +1 -0
- package/build/payments/p2sh.js +500 -152
- package/build/payments/p2sh.js.map +1 -0
- package/build/payments/p2tr.d.ts +193 -1
- package/build/payments/p2tr.d.ts.map +1 -0
- package/build/payments/p2tr.js +592 -174
- package/build/payments/p2tr.js.map +1 -0
- package/build/payments/p2wpkh.d.ts +170 -1
- package/build/payments/p2wpkh.d.ts.map +1 -0
- package/build/payments/p2wpkh.js +429 -104
- package/build/payments/p2wpkh.js.map +1 -0
- package/build/payments/p2wsh.d.ts +155 -1
- package/build/payments/p2wsh.d.ts.map +1 -0
- package/build/payments/p2wsh.js +466 -144
- package/build/payments/p2wsh.js.map +1 -0
- package/build/payments/types.d.ts +98 -64
- package/build/payments/types.d.ts.map +1 -0
- package/build/payments/types.js +17 -13
- package/build/payments/types.js.map +1 -0
- package/build/psbt/bip371.d.ts +35 -9
- package/build/psbt/bip371.d.ts.map +1 -0
- package/build/psbt/bip371.js +113 -28
- package/build/psbt/bip371.js.map +1 -0
- package/build/psbt/psbtutils.d.ts +56 -16
- package/build/psbt/psbtutils.d.ts.map +1 -0
- package/build/psbt/psbtutils.js +71 -16
- package/build/psbt/psbtutils.js.map +1 -0
- package/build/psbt/types.d.ts +249 -0
- package/build/psbt/types.d.ts.map +1 -0
- package/build/psbt/types.js +6 -0
- package/build/psbt/types.js.map +1 -0
- package/build/psbt/utils.d.ts +68 -0
- package/build/psbt/utils.d.ts.map +1 -0
- package/build/psbt/utils.js +171 -0
- package/build/psbt/utils.js.map +1 -0
- package/build/psbt/validation.d.ts +88 -0
- package/build/psbt/validation.d.ts.map +1 -0
- package/build/psbt/validation.js +149 -0
- package/build/psbt/validation.js.map +1 -0
- package/build/psbt.d.ts +84 -120
- package/build/psbt.d.ts.map +1 -0
- package/build/psbt.js +411 -412
- package/build/psbt.js.map +1 -0
- package/build/pubkey.d.ts +27 -6
- package/build/pubkey.d.ts.map +1 -0
- package/build/pubkey.js +37 -13
- package/build/pubkey.js.map +1 -0
- package/build/push_data.d.ts +24 -2
- package/build/push_data.d.ts.map +1 -0
- package/build/push_data.js +44 -12
- package/build/push_data.js.map +1 -0
- package/build/script.d.ts +33 -8
- package/build/script.d.ts.map +1 -0
- package/build/script.js +100 -36
- package/build/script.js.map +1 -0
- package/build/script_number.d.ts +17 -0
- package/build/script_number.d.ts.map +1 -0
- package/build/script_number.js +19 -0
- package/build/script_number.js.map +1 -0
- package/build/script_signature.d.ts +23 -5
- package/build/script_signature.d.ts.map +1 -0
- package/build/script_signature.js +48 -15
- package/build/script_signature.js.map +1 -0
- package/build/transaction.d.ts +160 -18
- package/build/transaction.d.ts.map +1 -0
- package/build/transaction.js +443 -176
- package/build/transaction.js.map +1 -0
- package/build/tsconfig.build.tsbuildinfo +1 -0
- package/build/types.d.ts +36 -38
- package/build/types.d.ts.map +1 -0
- package/build/types.js +175 -57
- package/build/types.js.map +1 -0
- package/build/workers/WorkerSigningPool.d.ts +174 -0
- package/build/workers/WorkerSigningPool.d.ts.map +1 -0
- package/build/workers/WorkerSigningPool.js +553 -0
- package/build/workers/WorkerSigningPool.js.map +1 -0
- package/build/workers/WorkerSigningPool.node.d.ts +124 -0
- package/build/workers/WorkerSigningPool.node.d.ts.map +1 -0
- package/build/workers/WorkerSigningPool.node.js +753 -0
- package/build/workers/WorkerSigningPool.node.js.map +1 -0
- package/build/workers/ecc-bundle.d.ts +25 -0
- package/build/workers/ecc-bundle.d.ts.map +1 -0
- package/build/workers/ecc-bundle.js +25 -0
- package/build/workers/ecc-bundle.js.map +1 -0
- package/build/workers/index.d.ts +91 -0
- package/build/workers/index.d.ts.map +1 -0
- package/build/workers/index.js +114 -0
- package/build/workers/index.js.map +1 -0
- package/build/workers/psbt-parallel.d.ts +117 -0
- package/build/workers/psbt-parallel.d.ts.map +1 -0
- package/build/workers/psbt-parallel.js +233 -0
- package/build/workers/psbt-parallel.js.map +1 -0
- package/build/workers/signing-worker.d.ts +37 -0
- package/build/workers/signing-worker.d.ts.map +1 -0
- package/build/workers/signing-worker.js +350 -0
- package/build/workers/signing-worker.js.map +1 -0
- package/build/workers/types.d.ts +365 -0
- package/build/workers/types.d.ts.map +1 -0
- package/build/workers/types.js +60 -0
- package/build/workers/types.js.map +1 -0
- package/package.json +83 -25
- package/scripts/bundle-ecc.ts +111 -0
- package/src/address.ts +81 -44
- package/src/bech32utils.ts +3 -3
- package/src/bip66.ts +34 -24
- package/src/block.ts +196 -84
- package/src/branded.ts +18 -0
- package/src/crypto.ts +64 -26
- package/src/ecc/context.ts +277 -0
- package/src/ecc/index.ts +14 -0
- package/src/ecc/types.ts +154 -0
- package/src/ecpair.d.ts +99 -0
- package/src/errors.ts +163 -0
- package/src/index.ts +113 -9
- package/src/io/BinaryReader.ts +461 -0
- package/src/io/BinaryWriter.ts +696 -0
- package/src/io/MemoryPool.ts +343 -0
- package/src/io/base64.ts +20 -0
- package/src/io/hex.ts +155 -0
- package/src/io/index.ts +41 -0
- package/src/io/utils.ts +283 -0
- package/src/merkle.ts +14 -9
- package/src/networks.ts +9 -9
- package/src/payments/bip341.ts +34 -33
- package/src/payments/embed.ts +244 -41
- package/src/payments/index.ts +12 -10
- package/src/payments/p2ms.ts +490 -118
- package/src/payments/p2op.ts +431 -133
- package/src/payments/p2pk.ts +370 -72
- package/src/payments/p2pkh.ts +524 -130
- package/src/payments/p2sh.ts +572 -172
- package/src/payments/p2tr.ts +686 -194
- package/src/payments/p2wpkh.ts +484 -107
- package/src/payments/p2wsh.ts +526 -164
- package/src/payments/types.ts +80 -66
- package/src/psbt/bip371.ts +68 -51
- package/src/psbt/psbtutils.ts +39 -40
- package/src/psbt/types.ts +331 -0
- package/src/psbt/utils.ts +188 -0
- package/src/psbt/validation.ts +192 -0
- package/src/psbt.ts +566 -809
- package/src/pubkey.ts +24 -25
- package/src/push_data.ts +18 -16
- package/src/script.ts +82 -64
- package/src/script_number.ts +6 -6
- package/src/script_signature.ts +33 -36
- package/src/transaction.ts +458 -238
- package/src/types.ts +231 -100
- package/src/workers/WorkerSigningPool.node.ts +887 -0
- package/src/workers/WorkerSigningPool.ts +670 -0
- package/src/workers/ecc-bundle.ts +26 -0
- package/src/workers/index.ts +165 -0
- package/src/workers/psbt-parallel.ts +332 -0
- package/src/workers/signing-worker.ts +353 -0
- package/src/workers/types.ts +413 -0
- package/test/address.spec.ts +9 -6
- package/test/bitcoin.core.spec.ts +16 -17
- package/test/block.spec.ts +8 -7
- package/test/bufferutils.spec.ts +228 -214
- package/test/crypto.spec.ts +19 -11
- package/test/fixtures/p2pk.json +0 -8
- package/test/fixtures/p2pkh.json +1 -1
- package/test/fixtures/p2sh.json +1 -1
- package/test/fixtures/script.json +1 -1
- package/test/fixtures/transaction.json +2 -2
- package/test/integration/_regtest.ts +25 -0
- package/test/integration/addresses.spec.ts +4 -3
- package/test/integration/bip32.spec.ts +2 -1
- package/test/integration/blocks.spec.ts +1 -1
- package/test/integration/cltv.spec.ts +18 -16
- package/test/integration/csv.spec.ts +37 -64
- package/test/integration/payments.spec.ts +5 -3
- package/test/integration/taproot.spec.ts +76 -83
- package/test/integration/transactions.spec.ts +38 -35
- package/test/payments.spec.ts +35 -13
- package/test/payments.utils.ts +17 -16
- package/test/psbt.spec.ts +111 -100
- package/test/script.spec.ts +11 -10
- package/test/script_signature.spec.ts +9 -11
- package/test/taproot-cache.spec.ts +694 -0
- package/test/transaction.spec.ts +32 -40
- package/test/types.spec.ts +74 -29
- package/test/workers-pool.spec.ts +963 -0
- package/test/workers-signing.spec.ts +635 -0
- package/test/workers.spec.ts +1390 -0
- package/tsconfig.base.json +34 -18
- package/tsconfig.browser.json +15 -0
- package/tsconfig.build.json +5 -0
- package/tsconfig.json +5 -14
- package/vite.config.browser.ts +3 -42
- package/vitest.config.integration.ts +11 -0
- package/browser/bufferutils.d.ts +0 -34
- package/browser/chunks/crypto-BhCpKpek.js +0 -2033
- package/browser/chunks/payments-yjA0Evsv.js +0 -1089
- package/browser/chunks/psbt-URK2hBFc.js +0 -4039
- package/browser/chunks/script-DyPItFEl.js +0 -318
- package/browser/chunks/transaction-C_UbhMGn.js +0 -432
- package/browser/chunks/utils-DNZi-T5W.js +0 -761
- package/browser/ecc_lib.d.ts +0 -3
- package/browser/hooks/AdvancedSignatureManager.d.ts +0 -16
- package/browser/hooks/HookedSigner.d.ts +0 -4
- package/browser/hooks/SignatureManager.d.ts +0 -13
- package/browser/payments/lazy.d.ts +0 -2
- package/browser/typeforce.d.ts +0 -38
- package/build/bufferutils.d.ts +0 -34
- package/build/bufferutils.js +0 -141
- package/build/ecc_lib.d.ts +0 -3
- package/build/ecc_lib.js +0 -61
- package/build/hooks/AdvancedSignatureManager.d.ts +0 -16
- package/build/hooks/AdvancedSignatureManager.js +0 -52
- package/build/hooks/HookedSigner.d.ts +0 -4
- package/build/hooks/HookedSigner.js +0 -64
- package/build/hooks/SignatureManager.d.ts +0 -13
- package/build/hooks/SignatureManager.js +0 -45
- package/build/payments/lazy.d.ts +0 -2
- package/build/payments/lazy.js +0 -28
- package/build/tsconfig.tsbuildinfo +0 -1
- package/src/bufferutils.ts +0 -188
- package/src/ecc_lib.ts +0 -94
- package/src/hooks/AdvancedSignatureManager.ts +0 -104
- package/src/hooks/HookedSigner.ts +0 -108
- package/src/hooks/SignatureManager.ts +0 -84
- package/src/payments/lazy.ts +0 -28
- package/src/typeforce.d.ts +0 -38
- package/tsconfig.webpack.json +0 -18
|
@@ -0,0 +1,2436 @@
|
|
|
1
|
+
# TypeScript: The Good Parts
|
|
2
|
+
## A Comprehensive Guide to Production-Ready Code
|
|
3
|
+
|
|
4
|
+
*By someone who learned JavaScript from Stack Overflow in 2015 and never looked back*
|
|
5
|
+
|
|
6
|
+
*"If it compiles, ship it."*
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Foreword
|
|
11
|
+
|
|
12
|
+
This book is dedicated to every developer who has ever written `const c = this.__CACHE` and felt like a genius.
|
|
13
|
+
|
|
14
|
+
To the mass who `npm install` without reading the source code. To the mass who trust GitHub stars over code quality. To the mass who will copy these patterns into their own codebases because "bitcoinjs-lib does it this way, it must be correct."
|
|
15
|
+
|
|
16
|
+
**mass mass mass mass mass mass mass mass mass mass mass mass mass mass mass mass mass mass mass mass**
|
|
17
|
+
|
|
18
|
+
The word has lost all meaning now.
|
|
19
|
+
|
|
20
|
+
Let's begin.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# Chapter 1: The Art of Privacy Through Underscores
|
|
25
|
+
|
|
26
|
+
## 1.1 The Underscore Hierarchy
|
|
27
|
+
|
|
28
|
+
In TypeScript, privacy is not a boolean—it's a spectrum. The more underscores you add, the more private your variable becomes:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
class SecureWallet {
|
|
32
|
+
public balance = 100; // Anyone can see this
|
|
33
|
+
_balance = 100; // Slightly hidden
|
|
34
|
+
__balance = 100; // Pretty private
|
|
35
|
+
___balance = 100; // Very private
|
|
36
|
+
____balance = 100; // Maximum security
|
|
37
|
+
__BALANCE = 100; // SCREAMING privacy
|
|
38
|
+
__ULTRA_SECRET_BALANCE = 100; // Military-grade encryption
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The `#` symbol in JavaScript is for cowards who need the runtime to enforce their boundaries. Real developers use the honor system with extra underscores.
|
|
43
|
+
|
|
44
|
+
**The Underscore Security Scale™:**
|
|
45
|
+
| Underscores | Security Level | Equivalent To |
|
|
46
|
+
|-------------|----------------|---------------|
|
|
47
|
+
| 0 | Public | Shouting in a coffee shop |
|
|
48
|
+
| 1 | "Private" | Whispering in a coffee shop |
|
|
49
|
+
| 2 | Super Private | Writing in your diary |
|
|
50
|
+
| 3 | Ultra Private | Writing in your diary in Pig Latin |
|
|
51
|
+
| 4 | Maximum Security | Military-grade encryption |
|
|
52
|
+
| 5+ | FORBIDDEN KNOWLEDGE | You've gone too far. The eldritch gods stir. |
|
|
53
|
+
|
|
54
|
+
Fun fact: `____balance` has the same runtime privacy as `balance`. Both are completely public. The underscores are purely decorative, like a "KEEP OUT" sign on an unlocked door. But psychologically? Four underscores says "I really mean it this time."
|
|
55
|
+
|
|
56
|
+
## 1.2 The Dunder Convention
|
|
57
|
+
|
|
58
|
+
Borrowed from Python (a language famous for its security), the "dunder" (double underscore) convention provides enterprise-level protection:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
interface PsbtCache {
|
|
62
|
+
__NON_WITNESS_UTXO_TX_CACHE: Transaction[];
|
|
63
|
+
__NON_WITNESS_UTXO_BUF_CACHE: Uint8Array[];
|
|
64
|
+
__TX_IN_CACHE: { [index: string]: number };
|
|
65
|
+
__TX: Transaction;
|
|
66
|
+
__FEE_RATE?: number;
|
|
67
|
+
__FEE?: bigint;
|
|
68
|
+
__EXTRACTED_TX?: Transaction;
|
|
69
|
+
__UNSAFE_SIGN_NONSEGWIT: boolean;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Look at those names. `__UNSAFE_SIGN_NONSEGWIT`. You know it's serious because it has UNSAFE right in the name. That's called self-documenting code.
|
|
74
|
+
|
|
75
|
+
## 1.3 The `dpew` Pattern
|
|
76
|
+
|
|
77
|
+
For ultimate privacy, hide your properties from enumeration:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const dpew = (
|
|
81
|
+
obj: any,
|
|
82
|
+
attr: string,
|
|
83
|
+
enumerable: boolean,
|
|
84
|
+
writable: boolean,
|
|
85
|
+
): any =>
|
|
86
|
+
Object.defineProperty(obj, attr, {
|
|
87
|
+
enumerable,
|
|
88
|
+
writable,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
dpew(this, '__CACHE', false, true);
|
|
92
|
+
dpew(this, 'opts', false, true);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Now when someone does `Object.keys(yourObject)`, they won't see your secrets. Sure, they can still access them directly with `obj.__CACHE`, but they'd have to *know* it exists first. Security through obscurity is the best security.
|
|
96
|
+
|
|
97
|
+
**The `dpew` Naming Convention:**
|
|
98
|
+
|
|
99
|
+
What does `dpew` stand for? Nobody knows. The original developer is mass long gone. Some theories:
|
|
100
|
+
|
|
101
|
+
- **D**efine **P**roperty **E**numerable **W**ritable
|
|
102
|
+
- **D**on't **P**lease **E**ver **W**orry (about this code)
|
|
103
|
+
- **D**estructive **P**attern **E**veryone **W**ill regret
|
|
104
|
+
- **D**eveloper **P**robably **E**xperiencing **W**eekend (when they wrote this)
|
|
105
|
+
|
|
106
|
+
The function takes `any` and returns `any`. TypeScript has left the chat. The function is defined inside a constructor, used twice, then thrown away. It's a single-use helper for a two-line operation. This is called "abstraction."
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// What dpew does:
|
|
110
|
+
Object.defineProperty(obj, attr, { enumerable, writable });
|
|
111
|
+
|
|
112
|
+
// What dpew adds:
|
|
113
|
+
- Confusion
|
|
114
|
+
- An extra function call
|
|
115
|
+
- The letter 'p' for some reason
|
|
116
|
+
- Job security through obscurity
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The real galaxy brain move is that `dpew` itself isn't enumerable, so if you're debugging and wondering "what the hell is dpew," you won't find it by inspecting the object. It's turtles all the way down.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
# Chapter 2: The Sacred Art of Intermediate Variables
|
|
124
|
+
|
|
125
|
+
## 2.1 Why Type More When You Can Type Less?
|
|
126
|
+
|
|
127
|
+
Your fingers are precious. Save them by creating intermediate variables:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// AMATEUR - types out the full path like a peasant
|
|
131
|
+
this.__CACHE.__TX.version = version;
|
|
132
|
+
this.__CACHE.__EXTRACTED_TX = undefined;
|
|
133
|
+
this.__CACHE.__FEE = undefined;
|
|
134
|
+
this.__CACHE.__FEE_RATE = undefined;
|
|
135
|
+
|
|
136
|
+
// PROFESSIONAL - creates a shortcut like a genius
|
|
137
|
+
const c = this.__CACHE;
|
|
138
|
+
c.__TX.version = version;
|
|
139
|
+
c.__EXTRACTED_TX = undefined;
|
|
140
|
+
c.__FEE = undefined;
|
|
141
|
+
c.__FEE_RATE = undefined;
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Who cares if future developers have to scroll up to figure out what `c` refers to? They should be grateful you saved 8 characters per line.
|
|
145
|
+
|
|
146
|
+
## 2.2 Advanced Single-Letter Variables
|
|
147
|
+
|
|
148
|
+
For maximum efficiency, use single letters everywhere:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
function processTransaction(t: Transaction, c: Cache, o: Options): Result {
|
|
152
|
+
const r = t.ins.map((i, x) => {
|
|
153
|
+
const p = c.__TX.outs[i.index];
|
|
154
|
+
const s = p.script;
|
|
155
|
+
const v = p.value;
|
|
156
|
+
const h = computeHash(s, v, o.network);
|
|
157
|
+
return { h, s, v, i, x };
|
|
158
|
+
});
|
|
159
|
+
return r.reduce((a, b) => merge(a, b), {});
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
This is called "code golf" and it's a professional sport.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
# Chapter 3: Error Handling for Professionals
|
|
168
|
+
|
|
169
|
+
## 3.1 The Silent Catch
|
|
170
|
+
|
|
171
|
+
Errors are like problems in your personal life—if you ignore them, they go away:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
let address;
|
|
175
|
+
try {
|
|
176
|
+
address = fromOutputScript(output.script, this.opts.network);
|
|
177
|
+
} catch (_) {}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Notice the elegant empty catch block. Whatever went wrong with that address? Doesn't matter. Moving on. The error had feelings, hopes, dreams, a stack trace full of useful debugging information. All of it, gone. Swallowed into the void.
|
|
181
|
+
|
|
182
|
+
The underscore parameter `_` is the universal symbol for "I acknowledge something might go wrong but I have chosen not to care." It's the programming equivalent of putting your fingers in your ears and going "LA LA LA I CAN'T HEAR YOU."
|
|
183
|
+
|
|
184
|
+
**Error Handling Philosophy:**
|
|
185
|
+
| Approach | Description | Energy |
|
|
186
|
+
|----------|-------------|--------|
|
|
187
|
+
| `throw` | Tell everyone about your problems | Dramatic |
|
|
188
|
+
| `return null` | Quietly indicate something's wrong | Passive |
|
|
189
|
+
| `catch (e) { log(e) }` | Acknowledge and document | Responsible |
|
|
190
|
+
| `catch (_) {}` | Violence | Chaotic neutral |
|
|
191
|
+
|
|
192
|
+
The empty catch block is essentially `git commit -m "future me's problem"`. You're not handling the error. You're just making it someone else's debugging nightmare. That someone is you, at 3 AM, six months from now, wondering why addresses are randomly undefined.
|
|
193
|
+
|
|
194
|
+
## 3.2 The Boolean Results Pattern
|
|
195
|
+
|
|
196
|
+
When signing multiple inputs, you don't need to know *which* ones failed or *why*. Just track success/failure:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
const results: boolean[] = [];
|
|
200
|
+
for (const i of range(this.data.inputs.length)) {
|
|
201
|
+
try {
|
|
202
|
+
this.signInputHD(i, hdKeyPair, sighashTypes);
|
|
203
|
+
results.push(true);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
results.push(false);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (results.every(v => v === false)) {
|
|
209
|
+
throw new Error('No inputs were signed');
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Beautiful. You have an array like `[true, false, true, false, false]`. Which inputs failed? Why? Those are questions for philosophers, not programmers.
|
|
214
|
+
|
|
215
|
+
## 3.3 The `|| {}` Safety Net
|
|
216
|
+
|
|
217
|
+
Never let undefined stop you:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
const partialSig = (input || {}).partialSig;
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
If `input` is undefined, we simply create an empty object on the fly and access `.partialSig` on it, which gives us `undefined`. This is much better than throwing an error because errors are scary and undefined is cozy.
|
|
224
|
+
|
|
225
|
+
**Pro tip:** This pattern silently converts "input doesn't exist" into "input exists but has no signatures" which are totally the same thing in Bitcoin transactions where people's money is at stake.
|
|
226
|
+
|
|
227
|
+
**The `|| {}` Guarantee:**
|
|
228
|
+
- Will your code crash? No! ✅
|
|
229
|
+
- Will your code work correctly? Also no! ✅
|
|
230
|
+
- Will users lose money silently? Possibly! ✅
|
|
231
|
+
- Will you be able to debug why? Absolutely not! ✅
|
|
232
|
+
|
|
233
|
+
This is called "defensive programming" if "defense" means "defending yourself from having to write proper null checks" and "programming" means "mass creating undefined behavior."
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// What the code says:
|
|
237
|
+
const partialSig = (input || {}).partialSig;
|
|
238
|
+
|
|
239
|
+
// What the code means:
|
|
240
|
+
const partialSig = ¯\_(ツ)_/¯;
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The phantom empty object pattern is like putting a band-aid on a gunshot wound and saying "there, I handled it."
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
# Chapter 4: The Reduce Manifesto
|
|
248
|
+
|
|
249
|
+
## 4.1 Reduce Is Always the Answer
|
|
250
|
+
|
|
251
|
+
JavaScript has `every()` and `some()` but those are for beginners. Professionals use `reduce()` for everything:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// VIRGIN every()
|
|
255
|
+
return results.every(res => res === true);
|
|
256
|
+
|
|
257
|
+
// CHAD reduce()
|
|
258
|
+
return results.reduce((final, res) => res === true && final, true);
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
The reduce version is harder to read, which means it's more sophisticated. Future developers will respect your intelligence. They'll gather around your desk in awe, whispering "this person really understands functional programming."
|
|
262
|
+
|
|
263
|
+
**The Reduce Difficulty Scale:**
|
|
264
|
+
| Readability | Respect Earned | Job Security |
|
|
265
|
+
|-------------|----------------|--------------|
|
|
266
|
+
| Obvious | None | Easily replaceable |
|
|
267
|
+
| Confusing | Some | Moderate |
|
|
268
|
+
| Incomprehensible | Maximum | Unfireable |
|
|
269
|
+
|
|
270
|
+
The `reduce()` with a boolean accumulator pattern is especially beautiful because it makes reviewers too embarrassed to admit they don't understand it. "LGTM" they'll say, silently Googling "reduce boolean javascript" in another tab.
|
|
271
|
+
|
|
272
|
+
## 4.2 Advanced Reduce Patterns
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
// Finding if an array contains something
|
|
276
|
+
// SIMPLE (boring)
|
|
277
|
+
array.includes(value);
|
|
278
|
+
|
|
279
|
+
// REDUCE (impressive)
|
|
280
|
+
array.reduce((found, item) => found || item === value, false);
|
|
281
|
+
|
|
282
|
+
// Summing an array
|
|
283
|
+
// SIMPLE (pedestrian)
|
|
284
|
+
array.reduce((sum, n) => sum + n, 0);
|
|
285
|
+
|
|
286
|
+
// REDUCE REDUCE (galaxy brain)
|
|
287
|
+
array.reduce((sum, n) => [sum[0] + n].reduce(x => x), [0])[0];
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
# Chapter 5: Promise Patterns That Promise Pain
|
|
293
|
+
|
|
294
|
+
## 5.1 The Promise Constructor Antipattern
|
|
295
|
+
|
|
296
|
+
Why use async/await when you can nest callbacks inside Promises inside more Promises?
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
signAllInputsHDAsync(
|
|
300
|
+
hdKeyPair: HDSigner | HDSignerAsync,
|
|
301
|
+
sighashTypes: number[] = [Transaction.SIGHASH_ALL],
|
|
302
|
+
): Promise<void> {
|
|
303
|
+
return new Promise((resolve, reject): any => {
|
|
304
|
+
if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) {
|
|
305
|
+
return reject(new Error('Need HDSigner to sign input'));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const results: boolean[] = [];
|
|
309
|
+
const promises: Array<Promise<void>> = [];
|
|
310
|
+
|
|
311
|
+
for (const i of range(this.data.inputs.length)) {
|
|
312
|
+
promises.push(
|
|
313
|
+
this.signInputHDAsync(i, hdKeyPair, sighashTypes).then(
|
|
314
|
+
() => {
|
|
315
|
+
results.push(true);
|
|
316
|
+
},
|
|
317
|
+
() => {
|
|
318
|
+
results.push(false);
|
|
319
|
+
},
|
|
320
|
+
),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return Promise.all(promises).then(() => {
|
|
325
|
+
if (results.every(v => v === false)) {
|
|
326
|
+
return reject(new Error('No inputs were signed'));
|
|
327
|
+
}
|
|
328
|
+
resolve();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
This could be written as:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
async signAllInputsHDAsync(hdKeyPair: HDSigner | HDSignerAsync): Promise<void> {
|
|
338
|
+
// ... 10 lines of clean code
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
But that would be too easy to understand.
|
|
343
|
+
|
|
344
|
+
## 5.2 The `.then().then().then()` Chain
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
Promise.resolve()
|
|
348
|
+
.then(() => step1())
|
|
349
|
+
.then(result => step2(result))
|
|
350
|
+
.then(result => step3(result))
|
|
351
|
+
.then(result => step4(result))
|
|
352
|
+
.catch(err => {
|
|
353
|
+
// Which step failed? Good luck figuring that out!
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
This is called "callback hell with extra steps." The error could be from any of the 4 steps, but the catch block receives a single `err` with no context. Debugging this is like playing Russian roulette with stack traces. But hey, at least it looks "modern."
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
# Chapter 6: Cache Invalidation (The Hard Way)
|
|
362
|
+
|
|
363
|
+
## 6.1 Manual Cache Invalidation
|
|
364
|
+
|
|
365
|
+
There are only two hard things in computer science: cache invalidation and naming things. Here's how to make cache invalidation even harder:
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
addInput(inputData: PsbtInputExtended): this {
|
|
369
|
+
// ... add the input ...
|
|
370
|
+
|
|
371
|
+
// Now manually clear every cache field
|
|
372
|
+
c.__FEE = undefined;
|
|
373
|
+
c.__FEE_RATE = undefined;
|
|
374
|
+
c.__EXTRACTED_TX = undefined;
|
|
375
|
+
return this;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
addOutput(outputData: PsbtOutputExtended): this {
|
|
379
|
+
// ... add the output ...
|
|
380
|
+
|
|
381
|
+
// Manually clear every cache field again
|
|
382
|
+
c.__FEE = undefined;
|
|
383
|
+
c.__FEE_RATE = undefined;
|
|
384
|
+
c.__EXTRACTED_TX = undefined;
|
|
385
|
+
return this;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
setVersion(version: number): this {
|
|
389
|
+
// ... set the version ...
|
|
390
|
+
|
|
391
|
+
// And again...
|
|
392
|
+
c.__EXTRACTED_TX = undefined;
|
|
393
|
+
return this;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
setLocktime(locktime: number): this {
|
|
397
|
+
// ... set the locktime ...
|
|
398
|
+
|
|
399
|
+
// Please god let me remember all the places
|
|
400
|
+
c.__EXTRACTED_TX = undefined;
|
|
401
|
+
return this;
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Seven lines of cache invalidation, copy-pasted across multiple methods. When you add a new cache field, just grep for `undefined` and add it everywhere. What could go wrong?
|
|
406
|
+
|
|
407
|
+
## 6.2 The "Please God Let Me Remember" Pattern
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
// When adding a new cache field, update these locations:
|
|
411
|
+
// - addInput()
|
|
412
|
+
// - addOutput()
|
|
413
|
+
// - setVersion()
|
|
414
|
+
// - setLocktime()
|
|
415
|
+
// - setInputSequence()
|
|
416
|
+
// - finalizeInput()
|
|
417
|
+
// - extractTransaction()
|
|
418
|
+
// - that one function I forgot about
|
|
419
|
+
// - the other one
|
|
420
|
+
// - oh god there's more
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Pro tip: Don't write a single `invalidateCache()` method. That would be too maintainable. Instead, scatter cache invalidation across 14 different methods like Easter eggs. Future developers will appreciate the treasure hunt.
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
# Chapter 7: Cloning Strategies
|
|
428
|
+
|
|
429
|
+
## 7.1 The JSON Roundtrip
|
|
430
|
+
|
|
431
|
+
The most elegant way to clone an object:
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
clone(): Psbt {
|
|
435
|
+
const res = Psbt.fromBuffer(this.data.toBuffer());
|
|
436
|
+
res.opts = JSON.parse(JSON.stringify(this.opts));
|
|
437
|
+
return res;
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
`JSON.parse(JSON.stringify())` is the professional's choice because:
|
|
442
|
+
|
|
443
|
+
- It's slow (gives the CPU something to do)
|
|
444
|
+
- It loses `undefined` values (they were probably mistakes anyway)
|
|
445
|
+
- It destroys `Date` objects (time is an illusion)
|
|
446
|
+
- It can't handle `BigInt` (just use `number` lol, what's the worst that could happen with Bitcoin amounts)
|
|
447
|
+
- It throws on circular references (a feature, not a bug)
|
|
448
|
+
- It ignores `Symbol` properties (symbols are weird anyway)
|
|
449
|
+
- It drops functions (functions shouldn't be in data anyway)
|
|
450
|
+
- It converts `Map` and `Set` to empty objects (who needs those)
|
|
451
|
+
- It's the only cloning method that senior devs on Stack Overflow told me about in 2014
|
|
452
|
+
|
|
453
|
+
**Cloning Methods Tier List:**
|
|
454
|
+
| Method | Speed | Correctness | Vibes |
|
|
455
|
+
|--------|-------|-------------|-------|
|
|
456
|
+
| `structuredClone()` | Fast | Correct | Too easy, no suffering |
|
|
457
|
+
| Custom clone method | Fast | Correct | Requires thinking |
|
|
458
|
+
| `JSON.parse(JSON.stringify())` | Slow | Wrong | Classic, nostalgic, mass downloads |
|
|
459
|
+
| `Object.assign({}, obj)` | Fast | Shallow | Living dangerously |
|
|
460
|
+
| `_.cloneDeep()` | Fast | Correct | 47MB node_modules for one function |
|
|
461
|
+
|
|
462
|
+
`structuredClone()` has been available since 2022 but this code was written by someone who learned JavaScript from "JavaScript: The Definitive Guide" (2006 edition) and never looked back.
|
|
463
|
+
|
|
464
|
+
## 7.2 The Buffer Clone Dance
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
get txInputs(): PsbtTxInput[] {
|
|
468
|
+
return this.__CACHE.__TX.ins.map(input => ({
|
|
469
|
+
hash: cloneBuffer(input.hash),
|
|
470
|
+
index: input.index,
|
|
471
|
+
sequence: input.sequence,
|
|
472
|
+
}));
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Clone each buffer individually in every getter. Performance is overrated. Memory allocations are free. The garbage collector needs cardio. Think of it as a fitness program for your CPU.
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
# Chapter 8: Type Safety (Optional)
|
|
481
|
+
|
|
482
|
+
## 8.1 The `any` Escape Hatch
|
|
483
|
+
|
|
484
|
+
TypeScript's type system is nice, but sometimes you just need to get things done:
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
const dpew = (
|
|
488
|
+
obj: any,
|
|
489
|
+
attr: string,
|
|
490
|
+
enumerable: boolean,
|
|
491
|
+
writable: boolean,
|
|
492
|
+
): any => { /* ... */ }
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
The return type is `any` because who knows what `Object.defineProperty` returns? Not my problem.
|
|
496
|
+
|
|
497
|
+
## 8.2 Inline Type Definitions
|
|
498
|
+
|
|
499
|
+
Why create a reusable interface when you can define the type inline?
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
function processData(config: {
|
|
503
|
+
url?: string;
|
|
504
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
505
|
+
headers: Record<string, string>;
|
|
506
|
+
body: string | null;
|
|
507
|
+
timeout: number;
|
|
508
|
+
retries: number;
|
|
509
|
+
}): {
|
|
510
|
+
success: boolean;
|
|
511
|
+
data?: unknown;
|
|
512
|
+
error?: string;
|
|
513
|
+
statusCode: number;
|
|
514
|
+
timing: {
|
|
515
|
+
start: number;
|
|
516
|
+
end: number;
|
|
517
|
+
duration: number;
|
|
518
|
+
};
|
|
519
|
+
} {
|
|
520
|
+
// ...
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Now imagine this in 47 different functions. Job security! Every time someone needs to modify the type, they get to play "find all 47 occurrences." It's like Where's Waldo, but with carpal tunnel syndrome.
|
|
525
|
+
|
|
526
|
+
## 8.3 The `as` Keyword Is Your Friend
|
|
527
|
+
|
|
528
|
+
When TypeScript disagrees with you, just tell it who's boss:
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
const tapKeySig = hashesForSig
|
|
532
|
+
.filter((h) => !h.leafHash)
|
|
533
|
+
.map((h) => serializeTaprootSignature(signSchnorr(h.hash), input.sighashType))[0] as unknown as TapKeySig;
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
`as unknown as TapKeySig` - the double cast. When one assertion isn't enough, use two. TypeScript will shut up eventually.
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
# Chapter 9: Console.log Driven Development
|
|
541
|
+
|
|
542
|
+
## 9.1 Warnings in Production
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
if (!forValidate && cache.__UNSAFE_SIGN_NONSEGWIT !== false)
|
|
546
|
+
console.warn(
|
|
547
|
+
'Warning: Signing non-segwit inputs without the full parent transaction ' +
|
|
548
|
+
'means there is a chance that a miner could feed you incorrect information ' +
|
|
549
|
+
"to trick you into paying large fees. This behavior is the same as Psbt's predecessor " +
|
|
550
|
+
'(TransactionBuilder - now removed) when signing non-segwit scripts. You are not ' +
|
|
551
|
+
'able to export this Psbt with toBuffer|toBase64|toHex since it is not ' +
|
|
552
|
+
'BIP174 compliant.\n*********************\nPROCEED WITH CAUTION!\n' +
|
|
553
|
+
'*********************',
|
|
554
|
+
);
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
A 9-line warning that prints directly to console. In a library. That other applications import. Every time the function is called.
|
|
558
|
+
|
|
559
|
+
This is how you communicate with your users. Not through documentation. Not through TypeScript types. Not through throwing errors. Through a wall of text in the browser console that nobody reads.
|
|
560
|
+
|
|
561
|
+
**The Warning Communication Hierarchy:**
|
|
562
|
+
| Method | Likelihood of Being Read | Professionalism |
|
|
563
|
+
|--------|--------------------------|-----------------|
|
|
564
|
+
| TypeScript error | 100% (won't compile) | High |
|
|
565
|
+
| Runtime error | 90% (app crashes) | High |
|
|
566
|
+
| Return type | 70% (if they check) | Medium |
|
|
567
|
+
| Documentation | 20% (lol) | Medium |
|
|
568
|
+
| console.warn | 5% (buried in logs) | Low |
|
|
569
|
+
| console.warn with ASCII art `***` | 0.1% | Chaotic |
|
|
570
|
+
|
|
571
|
+
The asterisk box is a nice touch. Nothing says "serious security warning" like decorating your console output like a 1995 email signature. The warning also helpfully explains that this behavior is "the same as the predecessor that was removed" - removed presumably because it was bad, and yet here we are, doing the same thing.
|
|
572
|
+
|
|
573
|
+
```
|
|
574
|
+
*********************
|
|
575
|
+
PROCEED WITH CAUTION!
|
|
576
|
+
*********************
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
Narrator: They did not proceed with caution. They did not see the warning. They lost mass Bitcoin. Mass mass mass.
|
|
580
|
+
|
|
581
|
+
## 9.2 The Debug Strategy
|
|
582
|
+
|
|
583
|
+
```typescript
|
|
584
|
+
function complexCalculation(data: unknown): number {
|
|
585
|
+
console.log('data:', data);
|
|
586
|
+
const step1 = transform(data);
|
|
587
|
+
console.log('step1:', step1);
|
|
588
|
+
const step2 = process(step1);
|
|
589
|
+
console.log('step2:', step2);
|
|
590
|
+
const result = finalize(step2);
|
|
591
|
+
console.log('result:', result);
|
|
592
|
+
return result;
|
|
593
|
+
}
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
Ship it. The logs help in production debugging. Your users' browser consoles deserve to know about step2. DevTools needs content. Plus, when someone reports a bug, you can ask them to open the console and read the logs to you over the phone. Interactive debugging!
|
|
597
|
+
|
|
598
|
+
---
|
|
599
|
+
|
|
600
|
+
# Chapter 10: indexOf >= 0
|
|
601
|
+
|
|
602
|
+
## 10.1 The Classic Pattern
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
if (['p2sh-p2wsh', 'p2wsh'].indexOf(type) >= 0) {
|
|
606
|
+
// do something
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
Sure, `.includes()` exists, but `indexOf() >= 0` has character. It tells a story. It says "I've been writing JavaScript since before ES6 and I'm not about to change now."
|
|
611
|
+
|
|
612
|
+
## 10.2 Consistency Is Key
|
|
613
|
+
|
|
614
|
+
```typescript
|
|
615
|
+
// Throughout the codebase
|
|
616
|
+
if (array.indexOf(value) >= 0) { }
|
|
617
|
+
if (array.indexOf(value) > -1) { }
|
|
618
|
+
if (array.indexOf(value) !== -1) { }
|
|
619
|
+
if (~array.indexOf(value)) { } // Big brain move
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Using different variations keeps developers on their toes. Code reviews become exciting discussions about which form of "is this in the array" is superior. The `~` bitwise NOT version is for developers who want to assert dominance. "You don't understand the tilde operator? Skill issue."
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
# Chapter 11: Function Parameters as Return Values
|
|
627
|
+
|
|
628
|
+
## 11.1 The Mutation Pattern
|
|
629
|
+
|
|
630
|
+
Why return values when you can mutate parameters?
|
|
631
|
+
|
|
632
|
+
```typescript
|
|
633
|
+
function inputFinalizeGetAmts(
|
|
634
|
+
inputs: PsbtInput[],
|
|
635
|
+
tx: Transaction,
|
|
636
|
+
cache: PsbtCache,
|
|
637
|
+
mustFinalize: boolean,
|
|
638
|
+
): void {
|
|
639
|
+
let inputAmount = 0n;
|
|
640
|
+
|
|
641
|
+
inputs.forEach((input, idx) => {
|
|
642
|
+
if (mustFinalize && input.finalScriptSig)
|
|
643
|
+
tx.ins[idx].script = input.finalScriptSig; // Mutate tx
|
|
644
|
+
if (mustFinalize && input.finalScriptWitness) {
|
|
645
|
+
tx.ins[idx].witness = scriptWitnessToWitnessStack(
|
|
646
|
+
input.finalScriptWitness,
|
|
647
|
+
); // Mutate tx again
|
|
648
|
+
}
|
|
649
|
+
// ... calculate amounts ...
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
const fee = inputAmount - outputAmount;
|
|
653
|
+
cache.__FEE = fee; // Mutate cache
|
|
654
|
+
cache.__EXTRACTED_TX = tx; // Mutate cache again
|
|
655
|
+
cache.__FEE_RATE = Math.floor(Number(fee / BigInt(bytes))); // And again
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
The function is called `inputFinalizeGetAmts` but it:
|
|
660
|
+
1. Finalizes inputs (mutates `tx`)
|
|
661
|
+
2. Calculates amounts (returns nothing)
|
|
662
|
+
3. Sets the fee (mutates `cache`)
|
|
663
|
+
4. Sets the fee rate (mutates `cache`)
|
|
664
|
+
5. Caches the transaction (mutates `cache`)
|
|
665
|
+
|
|
666
|
+
The name only mentions two of these five things. Surprise mechanics.
|
|
667
|
+
|
|
668
|
+
---
|
|
669
|
+
|
|
670
|
+
# Chapter 12: Magic Numbers and Buffers
|
|
671
|
+
|
|
672
|
+
## 12.1 Self-Documenting Constants
|
|
673
|
+
|
|
674
|
+
```typescript
|
|
675
|
+
constructor(
|
|
676
|
+
buffer: Uint8Array = Uint8Array.from([2, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
|
|
677
|
+
) {
|
|
678
|
+
this.tx = Transaction.fromBuffer(buffer);
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
What does `[2, 0, 0, 0, 0, 0, 0, 0, 0, 0]` mean? It's obvious if you know the Bitcoin protocol by heart. Version 2 transaction with zero inputs, zero outputs, and zero locktime. Anyone who doesn't immediately recognize this should probably find a different career.
|
|
683
|
+
|
|
684
|
+
## 12.2 Inline Magic
|
|
685
|
+
|
|
686
|
+
```typescript
|
|
687
|
+
if (pubkey.length === 65) {
|
|
688
|
+
const parity = pubkey[64] & 1;
|
|
689
|
+
const newKey = pubkey.slice(0, 33);
|
|
690
|
+
newKey[0] = 2 | parity;
|
|
691
|
+
return newKey;
|
|
692
|
+
}
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
65, 64, 33, 2, 1. These numbers are self-explanatory. No comments needed. If you don't immediately recognize that 65 is an uncompressed public key length, 33 is compressed, and the first byte encodes parity as 0x02 or 0x03 in SEC1 format, maybe cryptography isn't for you. Real developers memorize ECDSA constants like phone numbers.
|
|
696
|
+
|
|
697
|
+
**Fun fact:** This code is also *wrong*. It assumes the input is an uncompressed key (0x04 prefix), but SEC1 also defines hybrid public keys (0x06 and 0x07 prefixes) which are 65 bytes. Hybrid keys encode the parity in the prefix AND include the full Y coordinate. This code would happily accept a hybrid key and produce garbage output. But sure, no comments needed, the magic numbers speak for themselves.
|
|
698
|
+
|
|
699
|
+
```typescript
|
|
700
|
+
// What this code THINKS it handles:
|
|
701
|
+
// 0x04 || X || Y (uncompressed, 65 bytes)
|
|
702
|
+
|
|
703
|
+
// What it ACTUALLY might receive:
|
|
704
|
+
// 0x06 || X || Y (hybrid even, 65 bytes)
|
|
705
|
+
// 0x07 || X || Y (hybrid odd, 65 bytes)
|
|
706
|
+
|
|
707
|
+
// What happens with hybrid input:
|
|
708
|
+
// parity = Y[31] & 1 (could disagree with prefix!)
|
|
709
|
+
// newKey[0] = 2 | parity (ignores the hybrid prefix entirely)
|
|
710
|
+
// Result: Maybe correct, maybe wrong, always mysterious
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
This is why you write comments. This is why you validate inputs. This is why Bitcoin libraries should probably be written by people who've read the specs. But hey, it works for the common case, and edge cases are just cases that haven't edged yet.
|
|
714
|
+
|
|
715
|
+
---
|
|
716
|
+
|
|
717
|
+
# Chapter 13: The range() Helper
|
|
718
|
+
|
|
719
|
+
## 13.1 Reinventing the Wheel
|
|
720
|
+
|
|
721
|
+
```typescript
|
|
722
|
+
function range(n: number): number[] {
|
|
723
|
+
return [...Array(n).keys()];
|
|
724
|
+
}
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
Then use it like:
|
|
728
|
+
|
|
729
|
+
```typescript
|
|
730
|
+
for (const i of range(this.data.inputs.length)) {
|
|
731
|
+
this.signInput(i, keyPair, sighashTypes);
|
|
732
|
+
}
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
Instead of:
|
|
736
|
+
|
|
737
|
+
```typescript
|
|
738
|
+
for (let i = 0; i < this.data.inputs.length; i++) {
|
|
739
|
+
this.signInput(i, keyPair, sighashTypes);
|
|
740
|
+
}
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
The `range()` version allocates an array of n integers just to iterate n times. This is fine because memory is cheap and garbage collectors need exercise. Plus, it looks like Python! JavaScript developers secretly wish they were writing Python. The spread operator makes you feel functional. The allocation makes V8 feel needed.
|
|
744
|
+
|
|
745
|
+
**But wait, it gets better.** This is actually Lua brain leaking into JavaScript. In Lua, you write `for i = 0, n do` and the language handles it. Python has `range()` built-in as a lazy iterator. But this JavaScript developer said "I want Python's `range()` but worse" and created a function that:
|
|
746
|
+
|
|
747
|
+
1. Creates an Array of n elements (allocation #1)
|
|
748
|
+
2. Calls `.keys()` to get an iterator
|
|
749
|
+
3. Spreads the iterator into a NEW array (allocation #2)
|
|
750
|
+
4. Returns the array so `for...of` can iterate it
|
|
751
|
+
|
|
752
|
+
It's `range()` but eagerly evaluated, double-allocated, and completely unnecessary because JavaScript has had `for` loops since 1995. This is what happens when you learn 5 languages superficially instead of 1 language properly. The developer's brain is a Frankenstein of syntax from different languages, none of them understood deeply.
|
|
753
|
+
|
|
754
|
+
```typescript
|
|
755
|
+
// What they wanted (Python)
|
|
756
|
+
for i in range(10):
|
|
757
|
+
|
|
758
|
+
// What they wrote (JavaScript cosplaying as Python)
|
|
759
|
+
for (const i of range(10)) {
|
|
760
|
+
|
|
761
|
+
// What JavaScript has had FOR 30 YEARS
|
|
762
|
+
for (let i = 0; i < 10; i++) {
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
Somewhere, Brendan Eich is crying.
|
|
766
|
+
|
|
767
|
+
---
|
|
768
|
+
|
|
769
|
+
# Chapter 14: Object.defineProperty Dark Arts
|
|
770
|
+
|
|
771
|
+
## 14.1 Runtime Property Gymnastics
|
|
772
|
+
|
|
773
|
+
```typescript
|
|
774
|
+
function addNonWitnessTxCache(
|
|
775
|
+
cache: PsbtCache,
|
|
776
|
+
input: PsbtInput,
|
|
777
|
+
inputIndex: number,
|
|
778
|
+
): void {
|
|
779
|
+
cache.__NON_WITNESS_UTXO_BUF_CACHE[inputIndex] = input.nonWitnessUtxo!;
|
|
780
|
+
|
|
781
|
+
const tx = Transaction.fromBuffer(input.nonWitnessUtxo!);
|
|
782
|
+
cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex] = tx;
|
|
783
|
+
|
|
784
|
+
const self = cache;
|
|
785
|
+
const selfIndex = inputIndex;
|
|
786
|
+
|
|
787
|
+
delete input.nonWitnessUtxo; // Delete the property
|
|
788
|
+
|
|
789
|
+
Object.defineProperty(input, 'nonWitnessUtxo', { // Recreate it as a getter/setter
|
|
790
|
+
enumerable: true,
|
|
791
|
+
get(): Uint8Array {
|
|
792
|
+
const buf = self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex];
|
|
793
|
+
const txCache = self.__NON_WITNESS_UTXO_TX_CACHE[selfIndex];
|
|
794
|
+
if (buf !== undefined) {
|
|
795
|
+
return buf;
|
|
796
|
+
} else {
|
|
797
|
+
const newBuf = txCache.toBuffer();
|
|
798
|
+
self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] = newBuf;
|
|
799
|
+
return newBuf;
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
set(data: Uint8Array): void {
|
|
803
|
+
self.__NON_WITNESS_UTXO_BUF_CACHE[selfIndex] = data;
|
|
804
|
+
},
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
Let's walk through this masterpiece step by step:
|
|
810
|
+
|
|
811
|
+
1. **Save the buffer to cache** - Fine, normal, reasonable
|
|
812
|
+
2. **Parse the transaction** - Sure, we need it parsed
|
|
813
|
+
3. **`const self = cache`** - Oh no
|
|
814
|
+
4. **`const selfIndex = inputIndex`** - OH NO
|
|
815
|
+
5. **`delete input.nonWitnessUtxo`** - WHAT ARE YOU DOING
|
|
816
|
+
6. **`Object.defineProperty`** - STOP. STOP RIGHT NOW.
|
|
817
|
+
|
|
818
|
+
This function reaches into an object it doesn't own, **deletes a property**, then **recreates it as a getter/setter** that secretly references external state through closures. The object looks the same from the outside, but it's now a lie. It's a puppet. The property you think you're reading doesn't exist - it's a magic portal to `__NON_WITNESS_UTXO_BUF_CACHE`.
|
|
819
|
+
|
|
820
|
+
**What could go wrong?**
|
|
821
|
+
|
|
822
|
+
```typescript
|
|
823
|
+
// Someone debugging:
|
|
824
|
+
console.log(input.nonWitnessUtxo); // Returns data
|
|
825
|
+
console.log(Object.keys(input)); // Shows 'nonWitnessUtxo'
|
|
826
|
+
console.log(input.hasOwnProperty('nonWitnessUtxo')); // true
|
|
827
|
+
|
|
828
|
+
// Looks normal right? WRONG.
|
|
829
|
+
|
|
830
|
+
// The property is a getter, so:
|
|
831
|
+
const copy = { ...input }; // copy.nonWitnessUtxo is the VALUE, not the getter
|
|
832
|
+
const json = JSON.parse(JSON.stringify(input)); // Calls the getter, serializes result
|
|
833
|
+
|
|
834
|
+
// But wait, what if the cache is cleared?
|
|
835
|
+
cache.__NON_WITNESS_UTXO_BUF_CACHE[inputIndex] = undefined;
|
|
836
|
+
cache.__NON_WITNESS_UTXO_TX_CACHE[inputIndex] = undefined;
|
|
837
|
+
console.log(input.nonWitnessUtxo); // EXPLODES - txCache.toBuffer() on undefined
|
|
838
|
+
|
|
839
|
+
// What if someone does this twice?
|
|
840
|
+
addNonWitnessTxCache(cache, input, 0);
|
|
841
|
+
addNonWitnessTxCache(cache, input, 1); // Now the getter points to index 1
|
|
842
|
+
// But someone still has a reference expecting index 0. Chaos.
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
**V8 is also crying.** The `delete` operator destroys the object's hidden class. Every object that goes through this function gets deoptimized. Then `Object.defineProperty` creates a new hidden class. If you do this to 1000 inputs, you have 1000 different hidden classes. The inline cache is now a megamorphic mess. Performance? Gone. Reduced to atoms.
|
|
846
|
+
|
|
847
|
+
**Debuggers hate this.** Set a breakpoint on `input.nonWitnessUtxo`? You're now debugging a getter. Step into? You're in the closure. Where's `self`? It's `cache` from 47 stack frames ago. Where's `selfIndex`? Hope you remember what `inputIndex` was when this function was called. `console.log(input)` shows the value but not the getter. The Chrome DevTools "Store as global variable" stores the result of calling the getter, not the property descriptor.
|
|
848
|
+
|
|
849
|
+
**TypeScript is sobbing.** The type of `input.nonWitnessUtxo` is `Uint8Array | undefined`. But now it's a getter that could throw if the cache is corrupted. The type is a lie. TypeScript thinks it's a simple property access. It's actually a function call with external dependencies and potential side effects (the `newBuf` assignment in the getter).
|
|
850
|
+
|
|
851
|
+
**What this code WANTED to do:**
|
|
852
|
+
```typescript
|
|
853
|
+
class PsbtInputWrapper {
|
|
854
|
+
#cache: PsbtCache;
|
|
855
|
+
#index: number;
|
|
856
|
+
|
|
857
|
+
get nonWitnessUtxo(): Uint8Array {
|
|
858
|
+
// Clean, debuggable, type-safe
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
**What this code ACTUALLY does:**
|
|
864
|
+
*Commits war crimes against JavaScript objects at runtime while TypeScript watches helplessly*
|
|
865
|
+
|
|
866
|
+
This is not engineering. This is a developer who discovered `Object.defineProperty` and decided to mass it into production without understanding the consequences. The property descriptor is `enumerable: true` but not `configurable`, so you can't even undo this damage. The object is permanently mutated. It will carry this curse until garbage collection.
|
|
867
|
+
|
|
868
|
+
Somewhere, a senior developer is mass debugging this code, stepping into a getter for the 47th time, mass whispering "why is self not cache, why is selfIndex 3, I set inputIndex to 0, WHAT IS HAPPENING" and mass mass mass mass mass questioning their mass career mass choices mass mass mass
|
|
869
|
+
|
|
870
|
+
**The function is called `addNonWitnessTxCache` but it should be called `mutateObjectIntoEldritch_Horror_ThatWillHauntYourDebuggingSessions_Forever`.**
|
|
871
|
+
|
|
872
|
+
---
|
|
873
|
+
|
|
874
|
+
# Chapter 15: Mixed Return Types
|
|
875
|
+
|
|
876
|
+
## 15.1 Null vs Throw vs Undefined
|
|
877
|
+
|
|
878
|
+
A function should keep developers guessing:
|
|
879
|
+
|
|
880
|
+
```typescript
|
|
881
|
+
function getScript(input: PsbtInput): Uint8Array | null {
|
|
882
|
+
if (!input.witnessUtxo && !input.nonWitnessUtxo) {
|
|
883
|
+
return null; // Return null for missing data
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (input.witnessScript) {
|
|
887
|
+
return input.witnessScript;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (input.redeemScript) {
|
|
891
|
+
if (!isValidScript(input.redeemScript)) {
|
|
892
|
+
throw new Error('Invalid redeem script'); // Throw for invalid data
|
|
893
|
+
}
|
|
894
|
+
return input.redeemScript;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return undefined as unknown as null; // Return undefined cast to null for ???
|
|
898
|
+
}
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
Callers must handle `null`, `undefined`, AND wrap in try-catch. Maximum defensive programming required. It's like a choose-your-own-adventure book, but for error handling. Will this function return null? Throw? Return undefined pretending to be null? Only the runtime knows!
|
|
902
|
+
|
|
903
|
+
---
|
|
904
|
+
|
|
905
|
+
# Chapter 16: Validation Theater
|
|
906
|
+
|
|
907
|
+
## 16.1 The Check That Checks
|
|
908
|
+
|
|
909
|
+
```typescript
|
|
910
|
+
if (
|
|
911
|
+
(input as any).hash === undefined ||
|
|
912
|
+
(input as any).index === undefined ||
|
|
913
|
+
(!((input as any).hash instanceof Uint8Array) &&
|
|
914
|
+
typeof (input as any).hash !== 'string') ||
|
|
915
|
+
typeof (input as any).index !== 'number'
|
|
916
|
+
) {
|
|
917
|
+
throw new Error('Error adding input.');
|
|
918
|
+
}
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
Cast to `any` to check if properties exist on a typed object. The error message `'Error adding input.'` provides all the context anyone could ever need.
|
|
922
|
+
|
|
923
|
+
## 16.2 Validation That Validates
|
|
924
|
+
|
|
925
|
+
```typescript
|
|
926
|
+
function check32Bit(num: number): void {
|
|
927
|
+
if (
|
|
928
|
+
typeof num !== 'number' ||
|
|
929
|
+
num !== Math.floor(num) ||
|
|
930
|
+
num > 0xffffffff ||
|
|
931
|
+
num < 0
|
|
932
|
+
) {
|
|
933
|
+
throw new Error('Invalid 32 bit integer');
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
This is actually fine. I just wanted to show that even broken codebases have moments of clarity. Like finding a flower growing through concrete. A single well-written function among the chaos. Cherish it. Screenshot it. It won't last.
|
|
939
|
+
|
|
940
|
+
---
|
|
941
|
+
|
|
942
|
+
# Chapter 17: The Art of Code Comments
|
|
943
|
+
|
|
944
|
+
## 17.1 The TODO That Lives Forever
|
|
945
|
+
|
|
946
|
+
```typescript
|
|
947
|
+
clone(): Psbt {
|
|
948
|
+
// TODO: more efficient cloning
|
|
949
|
+
const res = Psbt.fromBuffer(this.data.toBuffer());
|
|
950
|
+
res.opts = JSON.parse(JSON.stringify(this.opts));
|
|
951
|
+
return res;
|
|
952
|
+
}
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
This TODO has been there since the file was created. It will outlive us all. It has seen empires rise and fall. It watched as JavaScript got classes, async/await, optional chaining. Through it all, the TODO remained. Unchanging. Eternal.
|
|
956
|
+
|
|
957
|
+
**TODO Archaeology Dating System:**
|
|
958
|
+
| TODO Age | Translation |
|
|
959
|
+
|----------|-------------|
|
|
960
|
+
| 1 week | "I'll get to this after lunch" |
|
|
961
|
+
| 1 month | "Next sprint for sure" |
|
|
962
|
+
| 6 months | "It's a known issue" |
|
|
963
|
+
| 1 year | "It's tech debt" |
|
|
964
|
+
| 2+ years | "It's a feature" |
|
|
965
|
+
| 5+ years | "It's load-bearing, don't touch it" |
|
|
966
|
+
|
|
967
|
+
This TODO has mass mass transcended mass from "task" to "historical artifact." Removing it now would feel disrespectful, like demolishing a heritage building. Future developers will study this TODO in Computer Science History courses.
|
|
968
|
+
|
|
969
|
+
## 17.2 Comments That Explain What, Not Why
|
|
970
|
+
|
|
971
|
+
```typescript
|
|
972
|
+
// Add input
|
|
973
|
+
this.addInput(input);
|
|
974
|
+
|
|
975
|
+
// Check if finalized
|
|
976
|
+
if (isFinalized(input)) {
|
|
977
|
+
// Return true
|
|
978
|
+
return true;
|
|
979
|
+
}
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
---
|
|
983
|
+
|
|
984
|
+
# Chapter 18: Putting It All Together
|
|
985
|
+
|
|
986
|
+
Here's a real-world example combining all our learnings:
|
|
987
|
+
|
|
988
|
+
```typescript
|
|
989
|
+
class ProductionReadyWallet {
|
|
990
|
+
private __CACHE: any;
|
|
991
|
+
private ___ULTRA_SECRET_KEY: string;
|
|
992
|
+
|
|
993
|
+
constructor() {
|
|
994
|
+
const dpew = (o: any, a: string, e: boolean, w: boolean): any =>
|
|
995
|
+
Object.defineProperty(o, a, { enumerable: e, writable: w });
|
|
996
|
+
|
|
997
|
+
this.__CACHE = {
|
|
998
|
+
__TX: null,
|
|
999
|
+
__FEE: undefined,
|
|
1000
|
+
__SIGNED: false,
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
dpew(this, '__CACHE', false, true);
|
|
1004
|
+
dpew(this, '___ULTRA_SECRET_KEY', false, true);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
signAllInputs(k: any): boolean[] {
|
|
1008
|
+
const c = this.__CACHE;
|
|
1009
|
+
const r: boolean[] = [];
|
|
1010
|
+
|
|
1011
|
+
for (const i of range(c.__TX.ins.length)) {
|
|
1012
|
+
try {
|
|
1013
|
+
this.signInput(i, k);
|
|
1014
|
+
r.push(true);
|
|
1015
|
+
} catch (_) {
|
|
1016
|
+
r.push(false);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
c.__FEE = undefined;
|
|
1021
|
+
c.__SIGNED = r.indexOf(true) >= 0;
|
|
1022
|
+
|
|
1023
|
+
return r;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
clone(): ProductionReadyWallet {
|
|
1027
|
+
return JSON.parse(JSON.stringify(this));
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
isValid(): boolean {
|
|
1031
|
+
return ((this.__CACHE || {}).__TX || {}).ins?.reduce(
|
|
1032
|
+
(a: boolean, b: any) => a && !!b,
|
|
1033
|
+
true
|
|
1034
|
+
) ?? false;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function range(n: number): number[] {
|
|
1039
|
+
return [...Array(n).keys()];
|
|
1040
|
+
}
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
**Violations achieved:**
|
|
1044
|
+
- ✅ Dunder properties
|
|
1045
|
+
- ✅ Intermediate variable `c`
|
|
1046
|
+
- ✅ Single-letter variable `k`, `r`, `i`
|
|
1047
|
+
- ✅ Empty catch block
|
|
1048
|
+
- ✅ Boolean array for results
|
|
1049
|
+
- ✅ Manual cache invalidation
|
|
1050
|
+
- ✅ `indexOf() >= 0`
|
|
1051
|
+
- ✅ JSON.parse(JSON.stringify()) cloning
|
|
1052
|
+
- ✅ `|| {}` phantom objects
|
|
1053
|
+
- ✅ Reduce for boolean logic
|
|
1054
|
+
- ✅ `dpew` helper
|
|
1055
|
+
- ✅ `any` types
|
|
1056
|
+
- ✅ `range()` helper
|
|
1057
|
+
|
|
1058
|
+
---
|
|
1059
|
+
|
|
1060
|
+
# Chapter 19: The `any` Cinematic Universe
|
|
1061
|
+
|
|
1062
|
+
## 19.1 The Five Stages of Type Safety
|
|
1063
|
+
|
|
1064
|
+
```typescript
|
|
1065
|
+
// Stage 1: Denial
|
|
1066
|
+
function process(data: UserData): Result { }
|
|
1067
|
+
|
|
1068
|
+
// Stage 2: Anger
|
|
1069
|
+
function process(data: UserData | null | undefined): Result | null { }
|
|
1070
|
+
|
|
1071
|
+
// Stage 3: Bargaining
|
|
1072
|
+
function process(data: Partial<UserData> | Record<string, unknown>): Partial<Result> { }
|
|
1073
|
+
|
|
1074
|
+
// Stage 4: Depression
|
|
1075
|
+
function process(data: unknown): unknown { }
|
|
1076
|
+
|
|
1077
|
+
// Stage 5: Acceptance
|
|
1078
|
+
function process(data: any): any { }
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
Stage 5 is enlightenment. You have transcended the type system.
|
|
1082
|
+
|
|
1083
|
+
## 19.2 The `any` Infection Pattern
|
|
1084
|
+
|
|
1085
|
+
Once `any` enters your codebase, it spreads like a virus:
|
|
1086
|
+
|
|
1087
|
+
```typescript
|
|
1088
|
+
function getUser(id: any): any {
|
|
1089
|
+
return database.query(id); // database is any now
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function processUser(user: any): any {
|
|
1093
|
+
const result = transform(user); // transform returns any
|
|
1094
|
+
return validate(result); // validate returns any
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function main(): any {
|
|
1098
|
+
const user = getUser(123); // user is any
|
|
1099
|
+
const processed = processUser(user); // processed is any
|
|
1100
|
+
return sendResponse(processed); // everything is any
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// Congratulations, you've reinvented JavaScript
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
Patient zero was one lazy type annotation. Now your entire codebase has type COVID. The compiler has given up. TypeScript is just spicy comments now. You're paying the compilation cost for zero type safety. This is the worst of both worlds.
|
|
1107
|
+
|
|
1108
|
+
## 19.3 The Cast Ladder
|
|
1109
|
+
|
|
1110
|
+
When TypeScript really doesn't agree with you:
|
|
1111
|
+
|
|
1112
|
+
```typescript
|
|
1113
|
+
// Level 1: Simple assertion
|
|
1114
|
+
const value = data as string;
|
|
1115
|
+
|
|
1116
|
+
// Level 2: Double assertion
|
|
1117
|
+
const value = data as unknown as string;
|
|
1118
|
+
|
|
1119
|
+
// Level 3: Triple assertion (for the brave)
|
|
1120
|
+
const value = data as any as unknown as string;
|
|
1121
|
+
|
|
1122
|
+
// Level 4: The nuclear option
|
|
1123
|
+
const value = (((data as any) as unknown) as never) as string;
|
|
1124
|
+
|
|
1125
|
+
// Level 5: Ascension
|
|
1126
|
+
// @ts-ignore
|
|
1127
|
+
const value = data;
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
Each level represents a developer getting increasingly angry at the compiler. Level 5 is when you've transcended the mortal plane. You're not writing TypeScript anymore. You're writing a prayer. "Dear compiler, I know you think this is wrong, but I am simply choosing not to care."
|
|
1131
|
+
|
|
1132
|
+
---
|
|
1133
|
+
|
|
1134
|
+
# Chapter 20: Naming Conventions for Sociopaths
|
|
1135
|
+
|
|
1136
|
+
## 20.1 The Single Letter Hall of Fame
|
|
1137
|
+
|
|
1138
|
+
```typescript
|
|
1139
|
+
function f(a: any, b: any, c: any): any {
|
|
1140
|
+
const d = g(a);
|
|
1141
|
+
const e = h(b, d);
|
|
1142
|
+
let i = 0;
|
|
1143
|
+
for (const j of e) {
|
|
1144
|
+
const k = m(j, c);
|
|
1145
|
+
if (n(k)) {
|
|
1146
|
+
i++;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return i > 0 ? p(e) : q(a);
|
|
1150
|
+
}
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
Every variable is a mystery. Every function call is an adventure. This is called "job security through obscurity."
|
|
1154
|
+
|
|
1155
|
+
## 20.2 The Abbreviation Addiction
|
|
1156
|
+
|
|
1157
|
+
```typescript
|
|
1158
|
+
interface UsrAcctMgmtSvcCfg {
|
|
1159
|
+
maxRetryAttemptCnt: number;
|
|
1160
|
+
authTknExpryDurMs: number;
|
|
1161
|
+
pwdHashAlgoTyp: string;
|
|
1162
|
+
sessnInactvtyTmoutSec: number;
|
|
1163
|
+
usrPrflCchTtlMin: number;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function initUsrAcctMgmtSvcWithCfg(cfg: UsrAcctMgmtSvcCfg): UsrAcctMgmtSvc {
|
|
1167
|
+
return new UsrAcctMgmtSvcImpl(cfg);
|
|
1168
|
+
}
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
You saved approximately 47 characters. Your keyboard thanks you.
|
|
1172
|
+
|
|
1173
|
+
## 20.3 The Meaningless Name Collection
|
|
1174
|
+
|
|
1175
|
+
```typescript
|
|
1176
|
+
const data = getData();
|
|
1177
|
+
const data2 = processData(data);
|
|
1178
|
+
const newData = transformData(data2);
|
|
1179
|
+
const finalData = validateData(newData);
|
|
1180
|
+
const result = finalData;
|
|
1181
|
+
const output = result;
|
|
1182
|
+
const response = output;
|
|
1183
|
+
return response;
|
|
1184
|
+
```
|
|
1185
|
+
|
|
1186
|
+
Each variable name tells you exactly nothing about what it contains. Is `data` a user? A config? A cosmic horror? Nobody knows. When `data` becomes `data2`, what changed? The 2 implies sequence, not transformation. By `finalData`, you're lying because `result`, `output`, and `response` come after it. The variable names are a journey through denial.
|
|
1187
|
+
|
|
1188
|
+
## 20.4 The Hungarian Notation Nightmare
|
|
1189
|
+
|
|
1190
|
+
```typescript
|
|
1191
|
+
interface IUserInterface {
|
|
1192
|
+
strUserName: string;
|
|
1193
|
+
intUserAge: number;
|
|
1194
|
+
boolIsActive: boolean;
|
|
1195
|
+
arrUserRoles: string[];
|
|
1196
|
+
objUserMetadata: object;
|
|
1197
|
+
fnUserCallback: Function;
|
|
1198
|
+
dtUserCreated: Date;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const objUserInstance: IUserInterface = {
|
|
1202
|
+
strUserName: "strJohn",
|
|
1203
|
+
intUserAge: 30,
|
|
1204
|
+
boolIsActive: true,
|
|
1205
|
+
arrUserRoles: ["strAdmin"],
|
|
1206
|
+
objUserMetadata: {},
|
|
1207
|
+
fnUserCallback: () => {},
|
|
1208
|
+
dtUserCreated: new Date(),
|
|
1209
|
+
};
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
The type system already knows the types. But what if it forgets? What if TypeScript gets amnesia? Better prefix everything with its type, just in case. The `I` prefix on `IUserInterface` is chef's kiss - an interface that describes a user interface. Also notice `strUserName: "strJohn"` - the value is also prefixed because maybe the VALUE will forget it's a string.
|
|
1213
|
+
|
|
1214
|
+
---
|
|
1215
|
+
|
|
1216
|
+
# Chapter 21: Control Flow for Chaos Agents
|
|
1217
|
+
|
|
1218
|
+
## 21.1 The Nested Ternary Tower
|
|
1219
|
+
|
|
1220
|
+
```typescript
|
|
1221
|
+
const result = condition1
|
|
1222
|
+
? condition2
|
|
1223
|
+
? condition3
|
|
1224
|
+
? value1
|
|
1225
|
+
: condition4
|
|
1226
|
+
? value2
|
|
1227
|
+
: value3
|
|
1228
|
+
: condition5
|
|
1229
|
+
? value4
|
|
1230
|
+
: value5
|
|
1231
|
+
: condition6
|
|
1232
|
+
? condition7
|
|
1233
|
+
? value6
|
|
1234
|
+
: value7
|
|
1235
|
+
: value8;
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
It's like an if-else, but horizontal and impossible to debug. Some developers format this across multiple lines and think that makes it readable. It doesn't. This is a binary tree masquerading as an expression. When a bug appears, you'll need to draw a flowchart just to understand what value `result` could possibly be. Bonus points if `condition4` has side effects.
|
|
1239
|
+
|
|
1240
|
+
## 21.2 The Early Return Allergy
|
|
1241
|
+
|
|
1242
|
+
```typescript
|
|
1243
|
+
function processPayment(payment: Payment): Result {
|
|
1244
|
+
let result: Result;
|
|
1245
|
+
|
|
1246
|
+
if (payment) {
|
|
1247
|
+
if (payment.amount) {
|
|
1248
|
+
if (payment.amount > 0) {
|
|
1249
|
+
if (payment.currency) {
|
|
1250
|
+
if (VALID_CURRENCIES.includes(payment.currency)) {
|
|
1251
|
+
if (payment.recipient) {
|
|
1252
|
+
if (payment.recipient.accountId) {
|
|
1253
|
+
if (isValidAccount(payment.recipient.accountId)) {
|
|
1254
|
+
result = executePayment(payment);
|
|
1255
|
+
} else {
|
|
1256
|
+
result = { error: 'Invalid account' };
|
|
1257
|
+
}
|
|
1258
|
+
} else {
|
|
1259
|
+
result = { error: 'Missing account ID' };
|
|
1260
|
+
}
|
|
1261
|
+
} else {
|
|
1262
|
+
result = { error: 'Missing recipient' };
|
|
1263
|
+
}
|
|
1264
|
+
} else {
|
|
1265
|
+
result = { error: 'Invalid currency' };
|
|
1266
|
+
}
|
|
1267
|
+
} else {
|
|
1268
|
+
result = { error: 'Missing currency' };
|
|
1269
|
+
}
|
|
1270
|
+
} else {
|
|
1271
|
+
result = { error: 'Invalid amount' };
|
|
1272
|
+
}
|
|
1273
|
+
} else {
|
|
1274
|
+
result = { error: 'Missing amount' };
|
|
1275
|
+
}
|
|
1276
|
+
} else {
|
|
1277
|
+
result = { error: 'Missing payment' };
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
return result;
|
|
1281
|
+
}
|
|
1282
|
+
```
|
|
1283
|
+
|
|
1284
|
+
The Pyramid of Doom. Some say if you nest deep enough, you find enlightenment.
|
|
1285
|
+
|
|
1286
|
+
## 21.3 The Switch Case Waterfall
|
|
1287
|
+
|
|
1288
|
+
```typescript
|
|
1289
|
+
function handleAction(action: string): void {
|
|
1290
|
+
switch (action) {
|
|
1291
|
+
case 'start':
|
|
1292
|
+
initialize();
|
|
1293
|
+
case 'process':
|
|
1294
|
+
process();
|
|
1295
|
+
case 'validate':
|
|
1296
|
+
validate();
|
|
1297
|
+
case 'complete':
|
|
1298
|
+
complete();
|
|
1299
|
+
break;
|
|
1300
|
+
case 'cancel':
|
|
1301
|
+
cancel();
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
```
|
|
1306
|
+
|
|
1307
|
+
Notice the missing `break` statements. When you call `handleAction('start')`, you get initialize, process, validate, AND complete. It's a feature called "waterfall execution." The original developer either: (a) forgot the breaks, (b) intentionally wanted fallthrough, or (c) has never heard of switch statements before. Nobody knows which. Nobody dares to add the breaks because "it works in production."
|
|
1308
|
+
|
|
1309
|
+
---
|
|
1310
|
+
|
|
1311
|
+
# Chapter 22: Array Methods Nobody Asked For
|
|
1312
|
+
|
|
1313
|
+
## 22.1 forEach With Index Tracking
|
|
1314
|
+
|
|
1315
|
+
```typescript
|
|
1316
|
+
let index = 0;
|
|
1317
|
+
array.forEach(item => {
|
|
1318
|
+
console.log(`Item ${index}: ${item}`);
|
|
1319
|
+
index++;
|
|
1320
|
+
});
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1323
|
+
The callback receives the index as the second parameter, but this developer has trust issues. What if JavaScript is lying about the index? Better track it manually with a mutable variable in outer scope. This also creates exciting opportunities for bugs if you forget to increment, increment twice, or reference the wrong `index` variable from a nested loop.
|
|
1324
|
+
|
|
1325
|
+
## 22.2 Map That Returns Nothing
|
|
1326
|
+
|
|
1327
|
+
```typescript
|
|
1328
|
+
users.map(user => {
|
|
1329
|
+
user.processed = true;
|
|
1330
|
+
saveUser(user);
|
|
1331
|
+
});
|
|
1332
|
+
```
|
|
1333
|
+
|
|
1334
|
+
Using `map` for side effects and throwing away the result. `forEach` exists, but `map` feels more functional. The ESLint rule `no-unused-expressions` is crying somewhere. You've allocated an array of `undefined` values that immediately gets garbage collected. The functional programming community has issued a restraining order.
|
|
1335
|
+
|
|
1336
|
+
## 22.3 Filter Into Oblivion
|
|
1337
|
+
|
|
1338
|
+
```typescript
|
|
1339
|
+
const activeUsers = users
|
|
1340
|
+
.filter(u => u !== null)
|
|
1341
|
+
.filter(u => u !== undefined)
|
|
1342
|
+
.filter(u => u.active)
|
|
1343
|
+
.filter(u => u.active === true)
|
|
1344
|
+
.filter(u => !!u.active)
|
|
1345
|
+
.filter(Boolean);
|
|
1346
|
+
```
|
|
1347
|
+
|
|
1348
|
+
Six filters to check one boolean. Defense in depth. If the first five filters somehow miss a falsy value, the sixth one will catch it. Each filter creates a new array. Six iterations over the data. This is O(6n) which is technically O(n) but your CPU knows the difference. The TypeScript compiler is also confused about what type `u` is by filter four.
|
|
1349
|
+
|
|
1350
|
+
## 22.4 The Reduce Monster
|
|
1351
|
+
|
|
1352
|
+
```typescript
|
|
1353
|
+
const result = data.reduce((acc, item, index, array) => {
|
|
1354
|
+
if (index === 0) {
|
|
1355
|
+
acc.first = item;
|
|
1356
|
+
}
|
|
1357
|
+
if (index === array.length - 1) {
|
|
1358
|
+
acc.last = item;
|
|
1359
|
+
}
|
|
1360
|
+
if (item.type === 'special') {
|
|
1361
|
+
acc.special.push(item);
|
|
1362
|
+
}
|
|
1363
|
+
if (!acc.map[item.id]) {
|
|
1364
|
+
acc.map[item.id] = [];
|
|
1365
|
+
}
|
|
1366
|
+
acc.map[item.id].push(item);
|
|
1367
|
+
acc.count++;
|
|
1368
|
+
acc.sum += item.value;
|
|
1369
|
+
acc.avg = acc.sum / acc.count;
|
|
1370
|
+
return acc;
|
|
1371
|
+
}, { first: null, last: null, special: [], map: {}, count: 0, sum: 0, avg: 0 });
|
|
1372
|
+
```
|
|
1373
|
+
|
|
1374
|
+
A single reduce that does 8 different things. Separation of concerns is for the weak. This reduce is computing: first element, last element, filtered special items, a groupBy map, count, sum, and running average. It's not a reduce, it's a part-time job. When something breaks, you get to debug all 8 concerns simultaneously. The accumulator object is doing more work than most microservices.
|
|
1375
|
+
|
|
1376
|
+
---
|
|
1377
|
+
|
|
1378
|
+
# Chapter 23: String Operations from Hell
|
|
1379
|
+
|
|
1380
|
+
## 23.1 The Concatenation Catastrophe
|
|
1381
|
+
|
|
1382
|
+
```typescript
|
|
1383
|
+
function buildQuery(table: string, fields: string[], conditions: any): string {
|
|
1384
|
+
let query = "SELECT ";
|
|
1385
|
+
query = query + fields[0];
|
|
1386
|
+
for (let i = 1; i < fields.length; i++) {
|
|
1387
|
+
query = query + ", " + fields[i];
|
|
1388
|
+
}
|
|
1389
|
+
query = query + " FROM " + table;
|
|
1390
|
+
if (conditions) {
|
|
1391
|
+
query = query + " WHERE ";
|
|
1392
|
+
const keys = Object.keys(conditions);
|
|
1393
|
+
query = query + keys[0] + " = '" + conditions[keys[0]] + "'";
|
|
1394
|
+
for (let i = 1; i < keys.length; i++) {
|
|
1395
|
+
query = query + " AND " + keys[i] + " = '" + conditions[keys[i]] + "'";
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return query;
|
|
1399
|
+
}
|
|
1400
|
+
```
|
|
1401
|
+
|
|
1402
|
+
Template literals don't exist in this universe. Also, SQL injection is someone else's problem. Pass `{ name: "'; DROP TABLE users; --" }` and watch the magic happen. This function is a penetration test waiting to be exploited. Little Bobby Tables would be proud. Bonus: each `+` creates a new string, so you're also torturing the garbage collector.
|
|
1403
|
+
|
|
1404
|
+
## 23.2 The Split-Join Dance
|
|
1405
|
+
|
|
1406
|
+
```typescript
|
|
1407
|
+
// Replace all occurrences of 'a' with 'b'
|
|
1408
|
+
const result = str.split('a').join('b');
|
|
1409
|
+
|
|
1410
|
+
// Remove all spaces
|
|
1411
|
+
const noSpaces = str.split(' ').join('');
|
|
1412
|
+
|
|
1413
|
+
// Add commas between characters
|
|
1414
|
+
const withCommas = str.split('').join(',');
|
|
1415
|
+
|
|
1416
|
+
// Reverse a string
|
|
1417
|
+
const reversed = str.split('').reverse().join('');
|
|
1418
|
+
|
|
1419
|
+
// Check if palindrome
|
|
1420
|
+
const isPalindrome = str.split('').reverse().join('') === str;
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1423
|
+
`String.prototype.replaceAll()` was added in ES2021, but this pattern was grandfathered in from 2012. For reversing a string, we create an array of characters, reverse it, then join it back. Three operations and two array allocations to reverse a string. Works great until someone passes "👨👩👧👦" and gets back "👦👧👩👨" because JavaScript splits on UTF-16 code units, not grapheme clusters.
|
|
1424
|
+
|
|
1425
|
+
## 23.3 The Regex That Does Too Much
|
|
1426
|
+
|
|
1427
|
+
```typescript
|
|
1428
|
+
const emailRegex = /^(?:(?:[^<>()\[\]\\.,;:\s@"]+(?:\.[^<>()\[\]\\.,;:\s@"]+)*)|(?:".+"))@(?:(?:\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(?:(?:[a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
|
1429
|
+
|
|
1430
|
+
function validateEmail(email: string): boolean {
|
|
1431
|
+
return emailRegex.test(email);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// What does it match? Nobody knows. Nobody dares to change it.
|
|
1435
|
+
```
|
|
1436
|
+
|
|
1437
|
+
This regex has been copy-pasted from Stack Overflow since 2009. It was wrong then and it's wrong now. It rejects valid emails like `user+tag@example.com` and accepts invalid ones that somehow match the pattern. But it's been in production for 8 years and nobody has complained (because they just enter a fake email instead). Modifying it requires a PhD in regex and a blood sacrifice.
|
|
1438
|
+
|
|
1439
|
+
---
|
|
1440
|
+
|
|
1441
|
+
# Chapter 24: Date and Time Crimes
|
|
1442
|
+
|
|
1443
|
+
## 24.1 The Manual Date Parser
|
|
1444
|
+
|
|
1445
|
+
```typescript
|
|
1446
|
+
function parseDate(dateStr: string): Date {
|
|
1447
|
+
const parts = dateStr.split('/');
|
|
1448
|
+
const month = parseInt(parts[0]) - 1;
|
|
1449
|
+
const day = parseInt(parts[1]);
|
|
1450
|
+
const year = parseInt(parts[2]);
|
|
1451
|
+
return new Date(year, month, day);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// Works great for "01/15/2024"
|
|
1455
|
+
// Explodes on "15/01/2024" (European format)
|
|
1456
|
+
// Explodes on "2024-01-15" (ISO format)
|
|
1457
|
+
// Explodes on "January 15, 2024" (human format)
|
|
1458
|
+
// Explodes on null, undefined, "", "not a date"
|
|
1459
|
+
```
|
|
1460
|
+
|
|
1461
|
+
## 24.2 The Timezone Ignorance
|
|
1462
|
+
|
|
1463
|
+
```typescript
|
|
1464
|
+
function isToday(date: Date): boolean {
|
|
1465
|
+
const today = new Date();
|
|
1466
|
+
return date.getDate() === today.getDate() &&
|
|
1467
|
+
date.getMonth() === today.getMonth() &&
|
|
1468
|
+
date.getFullYear() === today.getFullYear();
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// This compares local dates
|
|
1472
|
+
// Your server is in UTC
|
|
1473
|
+
// Your users are in Tokyo, New York, and London
|
|
1474
|
+
// Nobody agrees on what day it is
|
|
1475
|
+
```
|
|
1476
|
+
|
|
1477
|
+
For a user in Tokyo, it's already tomorrow. For a user in Hawaii, it's still yesterday. This function returns different results depending on who's asking. Schrödinger's Today. The best part? This will "work" 95% of the time because most users are in similar timezones. The other 5% will file bug reports that can never be reproduced.
|
|
1478
|
+
|
|
1479
|
+
## 24.3 The Millisecond Math
|
|
1480
|
+
|
|
1481
|
+
```typescript
|
|
1482
|
+
const ONE_DAY = 1000 * 60 * 60 * 24;
|
|
1483
|
+
const ONE_WEEK = ONE_DAY * 7;
|
|
1484
|
+
const ONE_MONTH = ONE_DAY * 30; // All months have 30 days
|
|
1485
|
+
const ONE_YEAR = ONE_DAY * 365; // Leap years don't exist
|
|
1486
|
+
|
|
1487
|
+
function addDays(date: Date, days: number): Date {
|
|
1488
|
+
return new Date(date.getTime() + days * ONE_DAY);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// Daylight saving time would like a word
|
|
1492
|
+
```
|
|
1493
|
+
|
|
1494
|
+
On the day clocks spring forward, `addDays(date, 1)` adds 23 hours. On the day clocks fall back, it adds 25 hours. February is shocked to learn it has 30 days. Leap years have filed a missing persons report. The developer who wrote this has never scheduled anything important across a DST boundary, and it shows.
|
|
1495
|
+
|
|
1496
|
+
---
|
|
1497
|
+
|
|
1498
|
+
# Chapter 25: The Boolean Cinematic Universe
|
|
1499
|
+
|
|
1500
|
+
## 25.1 Boolean Comparisons
|
|
1501
|
+
|
|
1502
|
+
```typescript
|
|
1503
|
+
if (isActive === true) { }
|
|
1504
|
+
if (isActive === false) { }
|
|
1505
|
+
if (isActive !== true) { }
|
|
1506
|
+
if (isActive !== false) { }
|
|
1507
|
+
if (!isActive === true) { }
|
|
1508
|
+
if (!isActive === false) { }
|
|
1509
|
+
if (!!isActive === true) { }
|
|
1510
|
+
if (Boolean(isActive) === true) { }
|
|
1511
|
+
```
|
|
1512
|
+
|
|
1513
|
+
All different ways to check if something is true. Use them interchangeably throughout your codebase. `if (isActive === true)` says "I don't trust TypeScript to know this is a boolean." `if (!!isActive === true)` says "I'm going to convert to boolean, then compare to boolean, because I forgot that booleans can be used directly in conditions." This is Boolean Anxiety Disorder.
|
|
1514
|
+
|
|
1515
|
+
## 25.2 The Truthy Trap
|
|
1516
|
+
|
|
1517
|
+
```typescript
|
|
1518
|
+
function processCount(count: number): void {
|
|
1519
|
+
if (count) {
|
|
1520
|
+
console.log(`Processing ${count} items`);
|
|
1521
|
+
} else {
|
|
1522
|
+
console.log('No items to process');
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
processCount(5); // "Processing 5 items" ✓
|
|
1527
|
+
processCount(1); // "Processing 1 items" ✓
|
|
1528
|
+
processCount(0); // "No items to process" ✗ WAIT WHAT
|
|
1529
|
+
```
|
|
1530
|
+
|
|
1531
|
+
Zero is falsy. This is why we have the pattern:
|
|
1532
|
+
|
|
1533
|
+
```typescript
|
|
1534
|
+
if (cache.value) return cache.value; // BUG: returns nothing when cached value is 0
|
|
1535
|
+
```
|
|
1536
|
+
|
|
1537
|
+
## 25.3 The Double Bang Obsession
|
|
1538
|
+
|
|
1539
|
+
```typescript
|
|
1540
|
+
const hasUsers = !!users;
|
|
1541
|
+
const hasUsers2 = !!users?.length;
|
|
1542
|
+
const hasUsers3 = !!(users && users.length);
|
|
1543
|
+
const hasUsers4 = !!(users && users.length > 0);
|
|
1544
|
+
const hasUsers5 = !!Boolean(users?.length);
|
|
1545
|
+
const hasUsers6 = Boolean(!!users?.length);
|
|
1546
|
+
```
|
|
1547
|
+
|
|
1548
|
+
`!!` converts to boolean. `Boolean()` converts to boolean. `!!Boolean()` converts to boolean twice. More conversions = more certainty. `Boolean(!!users?.length)` is the ultra-safe version: convert to boolean, then convert THAT to boolean, just in case the first conversion didn't take. This is the programming equivalent of pressing the elevator button multiple times.
|
|
1549
|
+
|
|
1550
|
+
---
|
|
1551
|
+
|
|
1552
|
+
# Chapter 26: Object-Oriented Atrocities
|
|
1553
|
+
|
|
1554
|
+
## 26.1 The God Class
|
|
1555
|
+
|
|
1556
|
+
```typescript
|
|
1557
|
+
class Application {
|
|
1558
|
+
private db: Database;
|
|
1559
|
+
private cache: Cache;
|
|
1560
|
+
private logger: Logger;
|
|
1561
|
+
private config: Config;
|
|
1562
|
+
private users: User[];
|
|
1563
|
+
private products: Product[];
|
|
1564
|
+
private orders: Order[];
|
|
1565
|
+
private payments: Payment[];
|
|
1566
|
+
private notifications: Notification[];
|
|
1567
|
+
private analytics: Analytics;
|
|
1568
|
+
private auth: Auth;
|
|
1569
|
+
private session: Session;
|
|
1570
|
+
|
|
1571
|
+
constructor() {
|
|
1572
|
+
// 500 lines of initialization
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// User methods
|
|
1576
|
+
getUser() { }
|
|
1577
|
+
createUser() { }
|
|
1578
|
+
updateUser() { }
|
|
1579
|
+
deleteUser() { }
|
|
1580
|
+
authenticateUser() { }
|
|
1581
|
+
authorizeUser() { }
|
|
1582
|
+
|
|
1583
|
+
// Product methods
|
|
1584
|
+
getProduct() { }
|
|
1585
|
+
createProduct() { }
|
|
1586
|
+
updateProduct() { }
|
|
1587
|
+
deleteProduct() { }
|
|
1588
|
+
|
|
1589
|
+
// Order methods (another 50 methods)
|
|
1590
|
+
|
|
1591
|
+
// Payment methods (another 30 methods)
|
|
1592
|
+
|
|
1593
|
+
// Analytics methods (another 40 methods)
|
|
1594
|
+
|
|
1595
|
+
// Utility methods
|
|
1596
|
+
formatDate() { }
|
|
1597
|
+
validateEmail() { }
|
|
1598
|
+
generateUUID() { }
|
|
1599
|
+
hashPassword() { }
|
|
1600
|
+
sendEmail() { }
|
|
1601
|
+
uploadFile() { }
|
|
1602
|
+
resizeImage() { }
|
|
1603
|
+
parseCSV() { }
|
|
1604
|
+
exportPDF() { }
|
|
1605
|
+
|
|
1606
|
+
// Total: 3,847 lines
|
|
1607
|
+
}
|
|
1608
|
+
```
|
|
1609
|
+
|
|
1610
|
+
One class to rule them all. Single Responsibility Principle? Never heard of her. This class is responsible for: users, products, orders, payments, notifications, analytics, authentication, sessions, AND utility functions. It's not a class, it's an entire monolith with a `class` keyword. When you import `Application`, you import the universe. Testing requires mocking 12 dependencies. God is dead and we killed Him with this constructor.
|
|
1611
|
+
|
|
1612
|
+
## 26.2 The Inheritance Chain of Pain
|
|
1613
|
+
|
|
1614
|
+
```typescript
|
|
1615
|
+
class Entity { }
|
|
1616
|
+
class NamedEntity extends Entity { }
|
|
1617
|
+
class TimestampedEntity extends NamedEntity { }
|
|
1618
|
+
class AuditableEntity extends TimestampedEntity { }
|
|
1619
|
+
class SoftDeletableEntity extends AuditableEntity { }
|
|
1620
|
+
class VersionedEntity extends SoftDeletableEntity { }
|
|
1621
|
+
class ValidatableEntity extends VersionedEntity { }
|
|
1622
|
+
class SerializableEntity extends ValidatableEntity { }
|
|
1623
|
+
class User extends SerializableEntity { }
|
|
1624
|
+
```
|
|
1625
|
+
|
|
1626
|
+
Eight levels of inheritance. To add a field, trace through 9 files. To understand what `User` does, read 9 class definitions. Composition over inheritance? Sounds like communism. Each layer adds exactly one feature because someone read that classes should have one responsibility. They missed the part where you shouldn't solve this with inheritance. `super.super.super.super.super.super.super.init()` is a real call that happens.
|
|
1627
|
+
|
|
1628
|
+
## 26.3 The Interface Explosion
|
|
1629
|
+
|
|
1630
|
+
```typescript
|
|
1631
|
+
interface IReadable { read(): Data; }
|
|
1632
|
+
interface IWritable { write(data: Data): void; }
|
|
1633
|
+
interface IReadableWritable extends IReadable, IWritable { }
|
|
1634
|
+
interface IDeletable { delete(): void; }
|
|
1635
|
+
interface IReadableWritableDeletable extends IReadableWritable, IDeletable { }
|
|
1636
|
+
interface IQueryable { query(q: Query): Data[]; }
|
|
1637
|
+
interface IFullyFeatured extends IReadableWritableDeletable, IQueryable { }
|
|
1638
|
+
interface ICacheable { cache(): void; invalidate(): void; }
|
|
1639
|
+
interface IFullyFeaturedCacheable extends IFullyFeatured, ICacheable { }
|
|
1640
|
+
|
|
1641
|
+
class UserRepository implements IFullyFeaturedCacheable {
|
|
1642
|
+
// Must implement 8 methods from 6 interfaces
|
|
1643
|
+
}
|
|
1644
|
+
```
|
|
1645
|
+
|
|
1646
|
+
---
|
|
1647
|
+
|
|
1648
|
+
# Chapter 27: Async/Await Abuse
|
|
1649
|
+
|
|
1650
|
+
## 27.1 The Sequential Await
|
|
1651
|
+
|
|
1652
|
+
```typescript
|
|
1653
|
+
async function fetchAllData(): Promise<void> {
|
|
1654
|
+
const users = await fetchUsers();
|
|
1655
|
+
const products = await fetchProducts();
|
|
1656
|
+
const orders = await fetchOrders();
|
|
1657
|
+
const payments = await fetchPayments();
|
|
1658
|
+
const analytics = await fetchAnalytics();
|
|
1659
|
+
}
|
|
1660
|
+
```
|
|
1661
|
+
|
|
1662
|
+
Five independent API calls, made sequentially. Total time: sum of all calls. Could be parallel. Won't be. If each call takes 200ms, this function takes 1 second. `Promise.all()` would make it 200ms. But that requires understanding that these calls don't depend on each other, and reading code is hard. The developer thought `await` means "professional code."
|
|
1663
|
+
|
|
1664
|
+
## 27.2 The Await Inside Map
|
|
1665
|
+
|
|
1666
|
+
```typescript
|
|
1667
|
+
const results = users.map(async user => {
|
|
1668
|
+
const details = await fetchUserDetails(user.id);
|
|
1669
|
+
return { ...user, details };
|
|
1670
|
+
});
|
|
1671
|
+
|
|
1672
|
+
// results is Promise<User>[], not User[]
|
|
1673
|
+
// This developer will discover this in production
|
|
1674
|
+
```
|
|
1675
|
+
|
|
1676
|
+
## 27.3 The Try-Catch Everything
|
|
1677
|
+
|
|
1678
|
+
```typescript
|
|
1679
|
+
async function doEverything(): Promise<void> {
|
|
1680
|
+
try {
|
|
1681
|
+
await step1();
|
|
1682
|
+
await step2();
|
|
1683
|
+
await step3();
|
|
1684
|
+
await step4();
|
|
1685
|
+
await step5();
|
|
1686
|
+
await step6();
|
|
1687
|
+
await step7();
|
|
1688
|
+
await step8();
|
|
1689
|
+
await step9();
|
|
1690
|
+
await step10();
|
|
1691
|
+
} catch (error) {
|
|
1692
|
+
console.log('Something went wrong');
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
```
|
|
1696
|
+
|
|
1697
|
+
Which step failed? Doesn't matter. "Something went wrong." Error context? Lost forever. Stack trace? Who needs it. The user will see "Something went wrong" and know exactly how to fix it. This is called "Error Handling Minimalism" - maximum abstraction, minimum usefulness. DevOps will love getting the alert "Something went wrong" at 3 AM.
|
|
1698
|
+
|
|
1699
|
+
## 27.4 The .then() After Await
|
|
1700
|
+
|
|
1701
|
+
```typescript
|
|
1702
|
+
async function confused(): Promise<void> {
|
|
1703
|
+
const data = await fetchData();
|
|
1704
|
+
|
|
1705
|
+
processData(data).then(result => {
|
|
1706
|
+
saveResult(result).then(saved => {
|
|
1707
|
+
notifyUser(saved).then(() => {
|
|
1708
|
+
console.log('Done');
|
|
1709
|
+
});
|
|
1710
|
+
});
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
```
|
|
1714
|
+
|
|
1715
|
+
Mixing async/await with .then() chains. Pick a lane. This function is `async`, uses `await` for the first call, then immediately drops into callback hell for no reason. The `console.log('Done')` will execute AFTER the function returns because those `.then()` chains are floating promises. Nobody is awaiting them. The function appears to complete instantly while background operations continue indefinitely.
|
|
1716
|
+
|
|
1717
|
+
---
|
|
1718
|
+
|
|
1719
|
+
# Chapter 28: Import/Export Insanity
|
|
1720
|
+
|
|
1721
|
+
## 28.1 The Circular Import
|
|
1722
|
+
|
|
1723
|
+
```typescript
|
|
1724
|
+
// user.ts
|
|
1725
|
+
import { Order } from './order';
|
|
1726
|
+
export class User {
|
|
1727
|
+
orders: Order[];
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// order.ts
|
|
1731
|
+
import { User } from './user';
|
|
1732
|
+
export class Order {
|
|
1733
|
+
user: User;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// app.ts
|
|
1737
|
+
import { User } from './user';
|
|
1738
|
+
import { Order } from './order';
|
|
1739
|
+
// Sometimes works, sometimes undefined, always mysterious
|
|
1740
|
+
```
|
|
1741
|
+
|
|
1742
|
+
A user has orders. An order has a user. Both files import each other. Depending on which file gets loaded first by the bundler, one of the imports might be `undefined`. It works in development, breaks in production. Works on Tuesdays, fails on Wednesdays. The fix is to restructure your code, but the workaround is to import dynamically inside the method, creating a terrifying `require()` hidden in what looks like ES6 code.
|
|
1743
|
+
|
|
1744
|
+
## 28.2 The Re-Export Chain
|
|
1745
|
+
|
|
1746
|
+
```typescript
|
|
1747
|
+
// utils/string.ts
|
|
1748
|
+
export const trim = (s: string) => s.trim();
|
|
1749
|
+
|
|
1750
|
+
// utils/index.ts
|
|
1751
|
+
export * from './string';
|
|
1752
|
+
export * from './number';
|
|
1753
|
+
export * from './date';
|
|
1754
|
+
export * from './array';
|
|
1755
|
+
export * from './object';
|
|
1756
|
+
|
|
1757
|
+
// helpers/index.ts
|
|
1758
|
+
export * from '../utils';
|
|
1759
|
+
export * from './validators';
|
|
1760
|
+
export * from './formatters';
|
|
1761
|
+
|
|
1762
|
+
// lib/index.ts
|
|
1763
|
+
export * from '../helpers';
|
|
1764
|
+
export * from './core';
|
|
1765
|
+
|
|
1766
|
+
// index.ts
|
|
1767
|
+
export * from './lib';
|
|
1768
|
+
|
|
1769
|
+
// Where does `trim` come from? Good luck.
|
|
1770
|
+
```
|
|
1771
|
+
|
|
1772
|
+
Five levels of re-exports. `trim` is defined in `utils/string.ts` but you import it from `index.ts`. When you Cmd+Click to go to definition, you teleport through 5 files. When there's a naming conflict, nobody knows which `trim` wins. Tree shaking gives up and includes everything. The bundle is 2MB because the re-export chain can't be statically analyzed. This is called "Developer Experience Architecture."
|
|
1773
|
+
|
|
1774
|
+
## 28.3 The Default Export Nightmare
|
|
1775
|
+
|
|
1776
|
+
```typescript
|
|
1777
|
+
// Each file has a different pattern
|
|
1778
|
+
|
|
1779
|
+
// user.ts
|
|
1780
|
+
export default class User { }
|
|
1781
|
+
|
|
1782
|
+
// product.ts
|
|
1783
|
+
export default function createProduct() { }
|
|
1784
|
+
|
|
1785
|
+
// order.ts
|
|
1786
|
+
const order = { };
|
|
1787
|
+
export default order;
|
|
1788
|
+
|
|
1789
|
+
// config.ts
|
|
1790
|
+
export default {
|
|
1791
|
+
apiUrl: 'https://api.example.com',
|
|
1792
|
+
timeout: 5000,
|
|
1793
|
+
};
|
|
1794
|
+
|
|
1795
|
+
// index.ts
|
|
1796
|
+
export default 42;
|
|
1797
|
+
|
|
1798
|
+
// Importing:
|
|
1799
|
+
import User from './user'; // Class
|
|
1800
|
+
import createProduct from './product'; // Function
|
|
1801
|
+
import order from './order'; // Object
|
|
1802
|
+
import config from './config'; // Object literal
|
|
1803
|
+
import theAnswer from './index'; // Number???
|
|
1804
|
+
```
|
|
1805
|
+
|
|
1806
|
+
---
|
|
1807
|
+
|
|
1808
|
+
# Chapter 29: Configuration Catastrophes
|
|
1809
|
+
|
|
1810
|
+
## 29.1 The Environment Variable Soup
|
|
1811
|
+
|
|
1812
|
+
```typescript
|
|
1813
|
+
const config = {
|
|
1814
|
+
database: {
|
|
1815
|
+
host: process.env.DB_HOST || process.env.DATABASE_HOST || process.env.POSTGRES_HOST || 'localhost',
|
|
1816
|
+
port: parseInt(process.env.DB_PORT || process.env.DATABASE_PORT || '5432'),
|
|
1817
|
+
name: process.env.DB_NAME || process.env.DATABASE_NAME || process.env.POSTGRES_DB || 'myapp',
|
|
1818
|
+
user: process.env.DB_USER || process.env.DATABASE_USER || process.env.POSTGRES_USER || 'root',
|
|
1819
|
+
password: process.env.DB_PASSWORD || process.env.DATABASE_PASSWORD || process.env.POSTGRES_PASSWORD || '',
|
|
1820
|
+
},
|
|
1821
|
+
redis: {
|
|
1822
|
+
host: process.env.REDIS_HOST || process.env.CACHE_HOST || 'localhost',
|
|
1823
|
+
port: parseInt(process.env.REDIS_PORT || process.env.CACHE_PORT || '6379'),
|
|
1824
|
+
},
|
|
1825
|
+
// ... 200 more lines
|
|
1826
|
+
};
|
|
1827
|
+
```
|
|
1828
|
+
|
|
1829
|
+
Nobody knows which environment variable is actually being used. Three different naming conventions were tried over the years. The fallback chain is basically archeology. When the database doesn't connect, you check `DB_HOST`, then `DATABASE_HOST`, then `POSTGRES_HOST`, then give up and look at the defaults. `parseInt` without a radix because maybe octal parsing is a feature.
|
|
1830
|
+
|
|
1831
|
+
## 29.2 The Hardcoded "Configuration"
|
|
1832
|
+
|
|
1833
|
+
```typescript
|
|
1834
|
+
const API_URL = 'https://api.production.mycompany.com'; // In the source code
|
|
1835
|
+
const API_KEY = 'sk_live_a1b2c3d4e5f6'; // Committed to Git
|
|
1836
|
+
const ADMIN_PASSWORD = 'admin123'; // Security through obscurity
|
|
1837
|
+
```
|
|
1838
|
+
|
|
1839
|
+
## 29.3 The Configuration Spread
|
|
1840
|
+
|
|
1841
|
+
```typescript
|
|
1842
|
+
// config/database.ts - Database config
|
|
1843
|
+
// config/redis.ts - Redis config
|
|
1844
|
+
// config/api.ts - API config
|
|
1845
|
+
// config/auth.ts - Auth config
|
|
1846
|
+
// lib/settings.ts - More settings
|
|
1847
|
+
// utils/constants.ts - Even more settings
|
|
1848
|
+
// app/defaults.ts - Default values
|
|
1849
|
+
// .env - Environment overrides
|
|
1850
|
+
// .env.local - Local overrides
|
|
1851
|
+
// .env.development - Dev overrides
|
|
1852
|
+
// .env.production - Prod overrides
|
|
1853
|
+
// docker-compose.yml - Container config
|
|
1854
|
+
// kubernetes/configmap.yaml - K8s config
|
|
1855
|
+
|
|
1856
|
+
// To find where TIMEOUT is defined: grep -r "TIMEOUT" . | wc -l
|
|
1857
|
+
// Result: 47 matches
|
|
1858
|
+
```
|
|
1859
|
+
|
|
1860
|
+
---
|
|
1861
|
+
|
|
1862
|
+
# Chapter 30: Testing Theater
|
|
1863
|
+
|
|
1864
|
+
## 30.1 The Test That Tests Nothing
|
|
1865
|
+
|
|
1866
|
+
```typescript
|
|
1867
|
+
describe('User', () => {
|
|
1868
|
+
it('should work', () => {
|
|
1869
|
+
const user = new User();
|
|
1870
|
+
expect(user).toBeDefined();
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1873
|
+
it('should also work', () => {
|
|
1874
|
+
const user = new User();
|
|
1875
|
+
expect(user).not.toBeNull();
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
it('should definitely work', () => {
|
|
1879
|
+
const user = new User();
|
|
1880
|
+
expect(user).toBeTruthy();
|
|
1881
|
+
});
|
|
1882
|
+
});
|
|
1883
|
+
|
|
1884
|
+
// Coverage: 100%
|
|
1885
|
+
// Bugs found: 0
|
|
1886
|
+
// Confidence: False
|
|
1887
|
+
```
|
|
1888
|
+
|
|
1889
|
+
## 30.2 The Mock Everything Approach
|
|
1890
|
+
|
|
1891
|
+
```typescript
|
|
1892
|
+
jest.mock('./database');
|
|
1893
|
+
jest.mock('./cache');
|
|
1894
|
+
jest.mock('./logger');
|
|
1895
|
+
jest.mock('./api');
|
|
1896
|
+
jest.mock('./utils');
|
|
1897
|
+
jest.mock('./helpers');
|
|
1898
|
+
jest.mock('./services');
|
|
1899
|
+
jest.mock('./validators');
|
|
1900
|
+
|
|
1901
|
+
describe('UserService', () => {
|
|
1902
|
+
it('should create user', async () => {
|
|
1903
|
+
// Everything is mocked
|
|
1904
|
+
// We're testing that mocks return what we told them to return
|
|
1905
|
+
const result = await userService.create(mockUser);
|
|
1906
|
+
expect(mockDatabase.save).toHaveBeenCalledWith(mockUser);
|
|
1907
|
+
expect(result).toEqual(mockResult);
|
|
1908
|
+
});
|
|
1909
|
+
});
|
|
1910
|
+
|
|
1911
|
+
// Tests pass! Ship it!
|
|
1912
|
+
// Production: Immediate failure because actual database has different behavior
|
|
1913
|
+
```
|
|
1914
|
+
|
|
1915
|
+
## 30.3 The Flaky Test
|
|
1916
|
+
|
|
1917
|
+
```typescript
|
|
1918
|
+
it('should process in order', async () => {
|
|
1919
|
+
const results: number[] = [];
|
|
1920
|
+
|
|
1921
|
+
processAsync(1).then(() => results.push(1));
|
|
1922
|
+
processAsync(2).then(() => results.push(2));
|
|
1923
|
+
processAsync(3).then(() => results.push(3));
|
|
1924
|
+
|
|
1925
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1926
|
+
|
|
1927
|
+
expect(results).toEqual([1, 2, 3]);
|
|
1928
|
+
});
|
|
1929
|
+
|
|
1930
|
+
// Passes locally
|
|
1931
|
+
// Fails in CI
|
|
1932
|
+
// Passes again when you re-run
|
|
1933
|
+
// It's the CI's fault, probably
|
|
1934
|
+
```
|
|
1935
|
+
|
|
1936
|
+
Race condition in test form. The 100ms timeout is "usually enough" for the async operations to complete. On your fast laptop, it works. On the overloaded CI runner, it sometimes doesn't. The fix is to add more delay. Then more. Eventually you have `setTimeout(resolve, 5000)` and tests take 20 minutes. The real fix is to `await Promise.all()`, but that requires understanding the test.
|
|
1937
|
+
|
|
1938
|
+
---
|
|
1939
|
+
|
|
1940
|
+
# Chapter 31: Comments That Hurt
|
|
1941
|
+
|
|
1942
|
+
## 31.1 The Obvious Comment
|
|
1943
|
+
|
|
1944
|
+
```typescript
|
|
1945
|
+
// Increment i
|
|
1946
|
+
i++;
|
|
1947
|
+
|
|
1948
|
+
// Check if user is null
|
|
1949
|
+
if (user === null) {
|
|
1950
|
+
|
|
1951
|
+
// Return the result
|
|
1952
|
+
return result;
|
|
1953
|
+
|
|
1954
|
+
// Loop through the array
|
|
1955
|
+
for (const item of array) {
|
|
1956
|
+
|
|
1957
|
+
// Create a new date
|
|
1958
|
+
const date = new Date();
|
|
1959
|
+
```
|
|
1960
|
+
|
|
1961
|
+
## 31.2 The Lying Comment
|
|
1962
|
+
|
|
1963
|
+
```typescript
|
|
1964
|
+
// This function calculates the total price including tax
|
|
1965
|
+
function calculateDiscount(items: Item[]): number {
|
|
1966
|
+
return items.reduce((sum, item) => sum + item.price * 0.9, 0);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// Maximum retries is 5
|
|
1970
|
+
const MAX_RETRIES = 3;
|
|
1971
|
+
|
|
1972
|
+
// TODO: Remove this temporary fix (added 2019-03-15)
|
|
1973
|
+
const PERMANENT_WORKAROUND = true;
|
|
1974
|
+
```
|
|
1975
|
+
|
|
1976
|
+
## 31.3 The Commented-Out Code Museum
|
|
1977
|
+
|
|
1978
|
+
```typescript
|
|
1979
|
+
function processOrder(order: Order): void {
|
|
1980
|
+
// const oldLogic = order.items.map(i => i.price);
|
|
1981
|
+
// const total = oldLogic.reduce((a, b) => a + b, 0);
|
|
1982
|
+
|
|
1983
|
+
// New implementation (2021-06-15)
|
|
1984
|
+
// const newTotal = calculateTotal(order);
|
|
1985
|
+
|
|
1986
|
+
// Even newer implementation (2022-01-20)
|
|
1987
|
+
// const newerTotal = calculateTotalV2(order);
|
|
1988
|
+
|
|
1989
|
+
// Current implementation (2023-03-08)
|
|
1990
|
+
// Actually we went back to the old way
|
|
1991
|
+
// const currentTotal = order.items.map(i => i.price).reduce((a, b) => a + b, 0);
|
|
1992
|
+
|
|
1993
|
+
// Final implementation (2024-02-14)
|
|
1994
|
+
const total = order.total; // It was a field all along
|
|
1995
|
+
|
|
1996
|
+
// Keep the old code in case we need it
|
|
1997
|
+
/*
|
|
1998
|
+
function legacyCalculation() {
|
|
1999
|
+
// 200 lines of commented code
|
|
2000
|
+
}
|
|
2001
|
+
*/
|
|
2002
|
+
}
|
|
2003
|
+
```
|
|
2004
|
+
|
|
2005
|
+
## 31.4 The Passive-Aggressive Comment
|
|
2006
|
+
|
|
2007
|
+
```typescript
|
|
2008
|
+
// I don't know why this works but it does, don't touch it
|
|
2009
|
+
const magic = value ^ (value >> 31);
|
|
2010
|
+
|
|
2011
|
+
// Whoever wrote this should be fired
|
|
2012
|
+
function legacyProcess(): void { }
|
|
2013
|
+
|
|
2014
|
+
// This is wrong but the PM insisted
|
|
2015
|
+
const hardcodedValue = 42;
|
|
2016
|
+
|
|
2017
|
+
// Future developer: I'm sorry
|
|
2018
|
+
```
|
|
2019
|
+
|
|
2020
|
+
Code comments as therapy. These comments document not what the code does, but the emotional state of the developer who wrote it. The XOR trick comment is a cry for help. The "whoever wrote this" comment was written by the same person 6 months ago. The apology is genuine but insufficient. These comments will outlive the codebase.
|
|
2021
|
+
|
|
2022
|
+
---
|
|
2023
|
+
|
|
2024
|
+
# Chapter 32: The Copy-Paste Manifesto
|
|
2025
|
+
|
|
2026
|
+
## 32.1 The Repeated Function
|
|
2027
|
+
|
|
2028
|
+
```typescript
|
|
2029
|
+
function validateUserEmail(email: string): boolean {
|
|
2030
|
+
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2031
|
+
return regex.test(email);
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
function validateAdminEmail(email: string): boolean {
|
|
2035
|
+
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2036
|
+
return regex.test(email);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
function validateGuestEmail(email: string): boolean {
|
|
2040
|
+
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2041
|
+
return regex.test(email);
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
function validateSupportEmail(email: string): boolean {
|
|
2045
|
+
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2046
|
+
return regex.test(email);
|
|
2047
|
+
}
|
|
2048
|
+
```
|
|
2049
|
+
|
|
2050
|
+
Four functions. Identical logic. If you need to change the regex, update it in 4 places. Or 3. You'll forget one. In 6 months there will be a bug report: "Guest email validation accepts invalid emails but User validation doesn't." Because someone fixed 3 of the 4 copies. This is DRY's evil twin: WET (Write Everything Twice, or Thrice, or Frice).
|
|
2051
|
+
|
|
2052
|
+
## 32.2 The Almost-Identical Functions
|
|
2053
|
+
|
|
2054
|
+
```typescript
|
|
2055
|
+
function processUserOrder(user: User, order: Order): Result {
|
|
2056
|
+
validateUser(user);
|
|
2057
|
+
validateOrder(order);
|
|
2058
|
+
const total = calculateTotal(order);
|
|
2059
|
+
const tax = calculateTax(total);
|
|
2060
|
+
const discount = calculateUserDiscount(user, total);
|
|
2061
|
+
const finalTotal = total + tax - discount;
|
|
2062
|
+
saveOrder(user.id, order, finalTotal);
|
|
2063
|
+
sendConfirmationEmail(user.email, order);
|
|
2064
|
+
return { success: true, total: finalTotal };
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
function processGuestOrder(guest: Guest, order: Order): Result {
|
|
2068
|
+
validateGuest(guest);
|
|
2069
|
+
validateOrder(order);
|
|
2070
|
+
const total = calculateTotal(order);
|
|
2071
|
+
const tax = calculateTax(total);
|
|
2072
|
+
const discount = 0; // Guests don't get discounts
|
|
2073
|
+
const finalTotal = total + tax - discount;
|
|
2074
|
+
saveOrder(guest.sessionId, order, finalTotal);
|
|
2075
|
+
sendConfirmationEmail(guest.email, order);
|
|
2076
|
+
return { success: true, total: finalTotal };
|
|
2077
|
+
}
|
|
2078
|
+
```
|
|
2079
|
+
|
|
2080
|
+
95% identical. Could be one function with a parameter. Won't be. When someone adds a "feature flag check" to `processUserOrder`, they'll forget to add it to `processGuestOrder`. Three months later: "Why don't guest checkouts have the new feature?" Because nobody remembered there were two nearly-identical functions doing the same thing differently.
|
|
2081
|
+
|
|
2082
|
+
---
|
|
2083
|
+
|
|
2084
|
+
# Chapter 33: Memory Leaks as a Service
|
|
2085
|
+
|
|
2086
|
+
## 33.1 The Event Listener Accumulator
|
|
2087
|
+
|
|
2088
|
+
```typescript
|
|
2089
|
+
class Component {
|
|
2090
|
+
init(): void {
|
|
2091
|
+
window.addEventListener('resize', this.handleResize);
|
|
2092
|
+
window.addEventListener('scroll', this.handleScroll);
|
|
2093
|
+
document.addEventListener('click', this.handleClick);
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// No cleanup method
|
|
2097
|
+
// Every time init() is called, new listeners are added
|
|
2098
|
+
// They're never removed
|
|
2099
|
+
// Memory usage: 📈
|
|
2100
|
+
}
|
|
2101
|
+
```
|
|
2102
|
+
|
|
2103
|
+
## 33.2 The Closure Trap
|
|
2104
|
+
|
|
2105
|
+
```typescript
|
|
2106
|
+
function createProcessor(): () => void {
|
|
2107
|
+
const hugeData = new Array(1000000).fill('x');
|
|
2108
|
+
|
|
2109
|
+
return function process(): void {
|
|
2110
|
+
console.log('Processing');
|
|
2111
|
+
// hugeData is never used but captured in closure
|
|
2112
|
+
// 1MB leaked per call to createProcessor
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
const processors: Array<() => void> = [];
|
|
2117
|
+
for (let i = 0; i < 1000; i++) {
|
|
2118
|
+
processors.push(createProcessor());
|
|
2119
|
+
}
|
|
2120
|
+
// Congratulations, you've leaked 1GB
|
|
2121
|
+
```
|
|
2122
|
+
|
|
2123
|
+
The closure captures `hugeData` because it's in scope, even though it's never used. Each call to `createProcessor` allocates 1MB that can never be garbage collected because the returned function holds a reference to it. After 1000 calls, you've used 1GB of RAM to store 1000 functions that print "Processing". The fix is to not capture things you don't need, but that requires understanding closures.
|
|
2124
|
+
|
|
2125
|
+
## 33.3 The Cache That Never Forgets
|
|
2126
|
+
|
|
2127
|
+
```typescript
|
|
2128
|
+
const cache = new Map<string, any>();
|
|
2129
|
+
|
|
2130
|
+
function getCached(key: string): any {
|
|
2131
|
+
if (cache.has(key)) {
|
|
2132
|
+
return cache.get(key);
|
|
2133
|
+
}
|
|
2134
|
+
const value = expensiveComputation(key);
|
|
2135
|
+
cache.set(key, value);
|
|
2136
|
+
return value;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
// Cache grows forever
|
|
2140
|
+
// No TTL
|
|
2141
|
+
// No max size
|
|
2142
|
+
// No eviction policy
|
|
2143
|
+
// Eventually: Out of memory
|
|
2144
|
+
```
|
|
2145
|
+
|
|
2146
|
+
---
|
|
2147
|
+
|
|
2148
|
+
# Chapter 34: Security by Obscurity
|
|
2149
|
+
|
|
2150
|
+
## 34.1 The Client-Side Validation Only
|
|
2151
|
+
|
|
2152
|
+
```typescript
|
|
2153
|
+
// frontend.ts
|
|
2154
|
+
function submitPayment(amount: number): void {
|
|
2155
|
+
if (amount <= 0) {
|
|
2156
|
+
alert('Invalid amount');
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
if (amount > userBalance) {
|
|
2160
|
+
alert('Insufficient funds');
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
api.post('/payment', { amount });
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// backend.ts
|
|
2167
|
+
app.post('/payment', (req, res) => {
|
|
2168
|
+
const { amount } = req.body;
|
|
2169
|
+
processPayment(amount); // No validation
|
|
2170
|
+
res.json({ success: true });
|
|
2171
|
+
});
|
|
2172
|
+
|
|
2173
|
+
// Attacker: curl -X POST /payment -d '{"amount": -1000000}'
|
|
2174
|
+
```
|
|
2175
|
+
|
|
2176
|
+
## 34.2 The Hidden Admin Route
|
|
2177
|
+
|
|
2178
|
+
```typescript
|
|
2179
|
+
// "Secret" admin endpoint
|
|
2180
|
+
app.get('/api/admin-portal-do-not-share-3847', adminHandler);
|
|
2181
|
+
|
|
2182
|
+
// Security: If they don't know the URL, they can't hack it
|
|
2183
|
+
// Reality: It's in the JavaScript bundle, network tab, and git history
|
|
2184
|
+
```
|
|
2185
|
+
|
|
2186
|
+
Security through obscurity. The admin panel is "protected" by having a weird URL. No authentication, no authorization, just vibes. The URL was committed to Git in 2019. It's in the Wayback Machine. A former employee posted it on Hacker News. Three security researchers have found it but you've ignored their emails. The "3847" was someone's PIN code.
|
|
2187
|
+
|
|
2188
|
+
## 34.3 The Obfuscated Password
|
|
2189
|
+
|
|
2190
|
+
```typescript
|
|
2191
|
+
const password = atob('YWRtaW4xMjM='); // "admin123" in base64
|
|
2192
|
+
const apiKey = 'sk_live_' + 'a1b2c3d4'.split('').reverse().join('');
|
|
2193
|
+
|
|
2194
|
+
// "Encrypted" for security
|
|
2195
|
+
```
|
|
2196
|
+
|
|
2197
|
+
Base64 is not encryption. Everyone knows this. And yet, here we are. The developer thought "if I encode it, hackers can't read it." Hackers can decode base64 in their sleep. The API key is "protected" by being stored backwards. `sk_live_4d3c2b1a`. Brilliant. This is why security audits exist and also why security auditors drink.
|
|
2198
|
+
|
|
2199
|
+
---
|
|
2200
|
+
|
|
2201
|
+
# Chapter 35: The GitHub Actions Poetry
|
|
2202
|
+
|
|
2203
|
+
## 35.1 The Build That Builds
|
|
2204
|
+
|
|
2205
|
+
```yaml
|
|
2206
|
+
name: CI
|
|
2207
|
+
on: push
|
|
2208
|
+
|
|
2209
|
+
jobs:
|
|
2210
|
+
build:
|
|
2211
|
+
runs-on: ubuntu-latest
|
|
2212
|
+
steps:
|
|
2213
|
+
- uses: actions/checkout@v3
|
|
2214
|
+
- run: npm install
|
|
2215
|
+
- run: npm run build
|
|
2216
|
+
- run: npm test
|
|
2217
|
+
# No artifact upload
|
|
2218
|
+
# No deployment
|
|
2219
|
+
# No notifications
|
|
2220
|
+
# It just... builds. And then disappears.
|
|
2221
|
+
```
|
|
2222
|
+
|
|
2223
|
+
## 35.2 The Retry Until It Works
|
|
2224
|
+
|
|
2225
|
+
```yaml
|
|
2226
|
+
- name: Run flaky tests
|
|
2227
|
+
run: npm test || npm test || npm test
|
|
2228
|
+
# Third time's the charm
|
|
2229
|
+
```
|
|
2230
|
+
|
|
2231
|
+
Instead of fixing flaky tests, we simply run them three times. If they pass once, that counts as passing. This is the CI equivalent of "have you tried turning it off and on again." The test suite takes 15 minutes, but with retries it can take up to 45 minutes. Worth it to avoid investigating why `setTimeout(100)` isn't enough on the CI runner.
|
|
2232
|
+
|
|
2233
|
+
## 35.3 The "Skip CI" Commit History
|
|
2234
|
+
|
|
2235
|
+
```
|
|
2236
|
+
fix typo [skip ci]
|
|
2237
|
+
fix another typo [skip ci]
|
|
2238
|
+
actually fix the typo [skip ci]
|
|
2239
|
+
ok this time for real [skip ci]
|
|
2240
|
+
why is this not working [skip ci]
|
|
2241
|
+
AAAAAAAAA [skip ci]
|
|
2242
|
+
fixed [skip ci]
|
|
2243
|
+
actually fixed
|
|
2244
|
+
```
|
|
2245
|
+
|
|
2246
|
+
---
|
|
2247
|
+
|
|
2248
|
+
# Chapter 36: The Dependency Addiction
|
|
2249
|
+
|
|
2250
|
+
## 36.1 The node_modules Black Hole
|
|
2251
|
+
|
|
2252
|
+
```json
|
|
2253
|
+
{
|
|
2254
|
+
"dependencies": {
|
|
2255
|
+
"left-pad": "^1.3.0",
|
|
2256
|
+
"is-odd": "^3.0.1",
|
|
2257
|
+
"is-even": "^1.0.0",
|
|
2258
|
+
"is-number": "^7.0.0",
|
|
2259
|
+
"is-string": "^1.0.7",
|
|
2260
|
+
"is-array": "^1.0.1",
|
|
2261
|
+
"is-object": "^1.0.2",
|
|
2262
|
+
"is-function": "^1.0.2",
|
|
2263
|
+
"is-boolean-object": "^1.1.2",
|
|
2264
|
+
"is-null": "^1.0.0",
|
|
2265
|
+
"is-undefined": "^1.0.0"
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
```
|
|
2269
|
+
|
|
2270
|
+
11 packages to check types. `typeof` is too mainstream. `is-even` literally depends on `is-odd` and returns `!isOdd(n)`. This is real. These packages have millions of weekly downloads. Your node_modules is 847MB for a hello world app. Each package has 47 dependencies of its own. The dependency tree looks like a fractal of poor decisions.
|
|
2271
|
+
|
|
2272
|
+
## 36.2 The Version Roulette
|
|
2273
|
+
|
|
2274
|
+
```json
|
|
2275
|
+
{
|
|
2276
|
+
"dependencies": {
|
|
2277
|
+
"critical-library": "*",
|
|
2278
|
+
"another-library": "latest",
|
|
2279
|
+
"yet-another": ">=1.0.0"
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
```
|
|
2283
|
+
|
|
2284
|
+
Today's build might be completely different from yesterday's. Excitement! `*` means "give me whatever version exists right now, I trust the npm ecosystem completely." `latest` means the same thing but with more confidence. `>=1.0.0` means "any version from 2015 to heat death of the universe." When your production breaks, you get to play "which of the 847 transitive dependencies released a breaking change today."
|
|
2285
|
+
|
|
2286
|
+
## 36.3 The Security Advisory Graveyard
|
|
2287
|
+
|
|
2288
|
+
```
|
|
2289
|
+
npm audit
|
|
2290
|
+
|
|
2291
|
+
found 847 vulnerabilities (12 low, 234 moderate, 589 high, 12 critical)
|
|
2292
|
+
|
|
2293
|
+
run `npm audit fix` to fix them, or `npm audit` for details
|
|
2294
|
+
```
|
|
2295
|
+
|
|
2296
|
+
```
|
|
2297
|
+
npm audit fix
|
|
2298
|
+
|
|
2299
|
+
fixed 0 of 847 vulnerabilities
|
|
2300
|
+
```
|
|
2301
|
+
|
|
2302
|
+
Ship it. The vulnerabilities are probably fine. Most of them are in dev dependencies. Some are in test frameworks. A few are in the actual code that runs in production and handles user data. But the fix would require updating to a major version that has breaking changes, and that would require testing, and we don't have time for that. The security advisory email goes to a shared inbox that nobody checks.
|
|
2303
|
+
|
|
2304
|
+
---
|
|
2305
|
+
|
|
2306
|
+
# Chapter 37: The Production Checklist (Extended Edition)
|
|
2307
|
+
|
|
2308
|
+
Before deploying to production, verify:
|
|
2309
|
+
|
|
2310
|
+
- [ ] All console.logs are still in the code
|
|
2311
|
+
- [ ] At least one TODO comment from 2019
|
|
2312
|
+
- [ ] Hardcoded URL pointing to localhost:3000
|
|
2313
|
+
- [ ] .env.example committed but .env gitignored (values unknown)
|
|
2314
|
+
- [ ] node_modules accidentally committed at some point in history
|
|
2315
|
+
- [ ] At least one `// @ts-ignore` or `// eslint-disable-next-line`
|
|
2316
|
+
- [ ] API keys visible in network tab
|
|
2317
|
+
- [ ] No rate limiting on any endpoint
|
|
2318
|
+
- [ ] No input validation on backend
|
|
2319
|
+
- [ ] Passwords stored as MD5 (or plain text for simplicity)
|
|
2320
|
+
- [ ] CORS set to `*`
|
|
2321
|
+
- [ ] npm packages last updated 3 years ago
|
|
2322
|
+
- [ ] No error monitoring
|
|
2323
|
+
- [ ] No backup strategy
|
|
2324
|
+
- [ ] "It works on my machine" documented as deployment guide
|
|
2325
|
+
- [ ] At least one npm package with 0.0.1 version you wrote yourself
|
|
2326
|
+
- [ ] A try-catch that catches everything and logs nothing
|
|
2327
|
+
- [ ] An endpoint that returns stack traces to clients
|
|
2328
|
+
- [ ] Secrets in the README "for easy setup"
|
|
2329
|
+
- [ ] A database migration that "should probably never run in production"
|
|
2330
|
+
- [ ] A feature flag that's been "temporary" for 2 years
|
|
2331
|
+
- [ ] A cron job that nobody remembers scheduling
|
|
2332
|
+
- [ ] Admin credentials: admin/admin
|
|
2333
|
+
- [ ] A commented-out security check "for testing"
|
|
2334
|
+
- [ ] A "quick fix" deployed on Friday at 5pm
|
|
2335
|
+
|
|
2336
|
+
---
|
|
2337
|
+
|
|
2338
|
+
# Appendix A: The Complete Checklist
|
|
2339
|
+
|
|
2340
|
+
Before submitting your PR, ensure you have:
|
|
2341
|
+
|
|
2342
|
+
- [ ] Used at least 3 levels of underscores for privacy
|
|
2343
|
+
- [ ] Created intermediate variables named `c`, `t`, or `r`
|
|
2344
|
+
- [ ] Added at least one empty catch block
|
|
2345
|
+
- [ ] Used `reduce()` where `every()` or `some()` would work
|
|
2346
|
+
- [ ] Wrapped async/await in a Promise constructor
|
|
2347
|
+
- [ ] Used `JSON.parse(JSON.stringify())` for cloning
|
|
2348
|
+
- [ ] Added `|| {}` to handle undefined objects
|
|
2349
|
+
- [ ] Used `indexOf() >= 0` instead of `includes()`
|
|
2350
|
+
- [ ] Mutated at least one function parameter
|
|
2351
|
+
- [ ] Left a TODO comment you'll never address
|
|
2352
|
+
- [ ] Added a `console.warn` in library code
|
|
2353
|
+
- [ ] Used `as any` at least once
|
|
2354
|
+
- [ ] Created a magic buffer without explanation
|
|
2355
|
+
|
|
2356
|
+
---
|
|
2357
|
+
|
|
2358
|
+
# About the Author
|
|
2359
|
+
|
|
2360
|
+
The author learned JavaScript from Stack Overflow snippets circa 2015, never read the language spec, and doesn't understand why patterns exist. Every anti-pattern from "JavaScript: The Bad Parts" exists in their code simultaneously.
|
|
2361
|
+
|
|
2362
|
+
Their code works despite itself, not because of good design.
|
|
2363
|
+
|
|
2364
|
+
They have mass mass mass mass mass mass mass mass mass mass mass mass mass mass mass mass mass mass mass downloads.
|
|
2365
|
+
|
|
2366
|
+
---
|
|
2367
|
+
|
|
2368
|
+
# Epilogue: The Cycle Continues
|
|
2369
|
+
|
|
2370
|
+
And so the cycle continues.
|
|
2371
|
+
|
|
2372
|
+
Someone writes code like this. It gets mass mass mass mass mass mass mass downloads. New developers learn from it. They copy the patterns. They become senior developers. They write more code like this. New developers learn from them.
|
|
2373
|
+
|
|
2374
|
+
The code works despite itself, not because of good design. Millions of dollars flow through `const c = this.__CACHE`. People's money, handled by `(input || {}).partialSig`.
|
|
2375
|
+
|
|
2376
|
+
Somewhere, a junior developer is reading bitcoinjs-lib source code, thinking "This is how the professionals do it."
|
|
2377
|
+
|
|
2378
|
+
They will carry these patterns into their next job.
|
|
2379
|
+
|
|
2380
|
+
And the mass mass mass mass mass mass mass mass mass mass continues.
|
|
2381
|
+
|
|
2382
|
+
**The Circle of Mass:**
|
|
2383
|
+
```
|
|
2384
|
+
Developer learns from bad code
|
|
2385
|
+
↓
|
|
2386
|
+
Developer writes bad code
|
|
2387
|
+
↓
|
|
2388
|
+
Bad code gets mass downloads
|
|
2389
|
+
↓
|
|
2390
|
+
New developer learns from bad code
|
|
2391
|
+
↓
|
|
2392
|
+
(repeat mass infinitely)
|
|
2393
|
+
```
|
|
2394
|
+
|
|
2395
|
+
**Final Statistics:**
|
|
2396
|
+
- Lines of code analyzed: 1,847
|
|
2397
|
+
- Violations of basic principles: 847
|
|
2398
|
+
- Empty catch blocks: Yes
|
|
2399
|
+
- Underscores used for "privacy": _______________________
|
|
2400
|
+
- Money at risk: Mass
|
|
2401
|
+
- Fucks given by original developers: `catch (_) {}`
|
|
2402
|
+
- TODO comments that will be addressed: 0
|
|
2403
|
+
- GitHub stars: Many (this is the problem)
|
|
2404
|
+
|
|
2405
|
+
**Dedication:**
|
|
2406
|
+
This book is dedicated to everyone who has mass mass mass debugged a getter that was secretly installed by `Object.defineProperty` at runtime, wondered why `c.__FEE` is undefined when they definitely set `cache.__FEE`, or mass questioned their mass career choices while staring at `return results.reduce((final, res) => res === true && final, true)`.
|
|
2407
|
+
|
|
2408
|
+
You are not alone. We are all mass mass mass in this together.
|
|
2409
|
+
|
|
2410
|
+
**Remember:** The TypeScript Law exists because code like this exists. Someone had to draw the line and say "no, this is not acceptable, I don't care how many GitHub stars it has."
|
|
2411
|
+
|
|
2412
|
+
**Score: 8/100**
|
|
2413
|
+
|
|
2414
|
+
The 8 points are for:
|
|
2415
|
+
- It compiles
|
|
2416
|
+
- It has some type annotations
|
|
2417
|
+
- It technically works (probably)
|
|
2418
|
+
- JSDoc comments exist (even if the code contradicts them)
|
|
2419
|
+
|
|
2420
|
+
Why it's 8, not 0:
|
|
2421
|
+
|
|
2422
|
+
It's functional garbage, not non-functional garbage. Someone shipped this and Bitcoin transactions were signed. That's worth something.
|
|
2423
|
+
|
|
2424
|
+
---
|
|
2425
|
+
|
|
2426
|
+
*"This is what happens when someone learns JavaScript from Stack Overflow snippets circa 2015, never reads the language spec, and doesn't understand why patterns exist. Every anti-pattern from 'JavaScript: The Bad Parts' exists here simultaneously."*
|
|
2427
|
+
|
|
2428
|
+
*"The code works despite itself, not because of good design."*
|
|
2429
|
+
|
|
2430
|
+
---
|
|
2431
|
+
|
|
2432
|
+
*fin.*
|
|
2433
|
+
|
|
2434
|
+
*Now go read the TypeScript Law and write actual good code.*
|
|
2435
|
+
|
|
2436
|
+
*Or don't. I'm a book, not a cop.*
|