@pkcprotocol/pkc-js 0.0.107 → 0.0.109
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/README.md +15 -0
- package/dist/browser/clients/base-client-manager.d.ts +1 -1
- package/dist/browser/clients/base-client-manager.js +6 -2
- package/dist/browser/clients/base-client-manager.js.map +1 -1
- package/dist/browser/clients/rpc-client/rpc-schema-util.d.ts +16 -0
- package/dist/browser/clients/rpc-client/schema.d.ts +16 -0
- package/dist/browser/community/remote-community.d.ts +1 -0
- package/dist/browser/community/remote-community.js +1 -0
- package/dist/browser/community/remote-community.js.map +1 -1
- package/dist/browser/community/rpc-local-community.js +13 -0
- package/dist/browser/community/rpc-local-community.js.map +1 -1
- package/dist/browser/community/schema.d.ts +1369 -1
- package/dist/browser/community/schema.js +90 -4
- package/dist/browser/community/schema.js.map +1 -1
- package/dist/browser/community/types.d.ts +13 -2
- package/dist/browser/constants.d.ts +1 -0
- package/dist/browser/constants.js +6 -0
- package/dist/browser/constants.js.map +1 -1
- package/dist/browser/errors.d.ts +15 -1
- package/dist/browser/errors.js +15 -0
- package/dist/browser/errors.js.map +1 -1
- package/dist/browser/generated-version.d.ts +1 -1
- package/dist/browser/generated-version.js +1 -1
- package/dist/browser/helia/helia-for-pkc.js +41 -4
- package/dist/browser/helia/helia-for-pkc.js.map +1 -1
- package/dist/browser/helia/util.js +4 -1
- package/dist/browser/helia/util.js.map +1 -1
- package/dist/browser/index-with-compile-cache.d.ts +559 -0
- package/dist/browser/index.d.ts +1120 -0
- package/dist/browser/index.js +5 -0
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/logger.js +10 -1
- package/dist/browser/logger.js.map +1 -1
- package/dist/browser/object-key-util.d.ts +2 -0
- package/dist/browser/object-key-util.js +12 -0
- package/dist/browser/object-key-util.js.map +1 -0
- package/dist/browser/pages/page-sort-options.d.ts +14 -0
- package/dist/browser/pages/page-sort-options.js +92 -0
- package/dist/browser/pages/page-sort-options.js.map +1 -0
- package/dist/browser/pages/page-sort-scoring.d.ts +35 -0
- package/dist/browser/pages/page-sort-scoring.js +124 -0
- package/dist/browser/pages/page-sort-scoring.js.map +1 -0
- package/dist/browser/pages/pages-client-manager.d.ts +1 -0
- package/dist/browser/pages/pages-client-manager.js +16 -4
- package/dist/browser/pages/pages-client-manager.js.map +1 -1
- package/dist/browser/pages/pkc-js-page-sorts/active.d.ts +3 -0
- package/dist/browser/pages/pkc-js-page-sorts/active.js +15 -0
- package/dist/browser/pages/pkc-js-page-sorts/active.js.map +1 -0
- package/dist/browser/pages/pkc-js-page-sorts/best.d.ts +3 -0
- package/dist/browser/pages/pkc-js-page-sorts/best.js +10 -0
- package/dist/browser/pages/pkc-js-page-sorts/best.js.map +1 -0
- package/dist/browser/pages/pkc-js-page-sorts/controversial.d.ts +3 -0
- package/dist/browser/pages/pkc-js-page-sorts/controversial.js +10 -0
- package/dist/browser/pages/pkc-js-page-sorts/controversial.js.map +1 -0
- package/dist/browser/pages/pkc-js-page-sorts/hot.d.ts +3 -0
- package/dist/browser/pages/pkc-js-page-sorts/hot.js +10 -0
- package/dist/browser/pages/pkc-js-page-sorts/hot.js.map +1 -0
- package/dist/browser/pages/pkc-js-page-sorts/index.d.ts +2 -0
- package/dist/browser/pages/pkc-js-page-sorts/index.js +37 -0
- package/dist/browser/pages/pkc-js-page-sorts/index.js.map +1 -0
- package/dist/browser/pages/pkc-js-page-sorts/new-flat.d.ts +3 -0
- package/dist/browser/pages/pkc-js-page-sorts/new-flat.js +12 -0
- package/dist/browser/pages/pkc-js-page-sorts/new-flat.js.map +1 -0
- package/dist/browser/pages/pkc-js-page-sorts/new.d.ts +3 -0
- package/dist/browser/pages/pkc-js-page-sorts/new.js +10 -0
- package/dist/browser/pages/pkc-js-page-sorts/new.js.map +1 -0
- package/dist/browser/pages/pkc-js-page-sorts/old-flat.d.ts +3 -0
- package/dist/browser/pages/pkc-js-page-sorts/old-flat.js +12 -0
- package/dist/browser/pages/pkc-js-page-sorts/old-flat.js.map +1 -0
- package/dist/browser/pages/pkc-js-page-sorts/old.d.ts +3 -0
- package/dist/browser/pages/pkc-js-page-sorts/old.js +10 -0
- package/dist/browser/pages/pkc-js-page-sorts/old.js.map +1 -0
- package/dist/browser/pages/pkc-js-page-sorts/top-all.d.ts +3 -0
- package/dist/browser/pages/pkc-js-page-sorts/top-all.js +10 -0
- package/dist/browser/pages/pkc-js-page-sorts/top-all.js.map +1 -0
- package/dist/browser/pages/pkc-js-page-sorts/top-day.d.ts +3 -0
- package/dist/browser/pages/pkc-js-page-sorts/top-day.js +11 -0
- package/dist/browser/pages/pkc-js-page-sorts/top-day.js.map +1 -0
- package/dist/browser/pages/pkc-js-page-sorts/top-hour.d.ts +3 -0
- package/dist/browser/pages/pkc-js-page-sorts/top-hour.js +11 -0
- package/dist/browser/pages/pkc-js-page-sorts/top-hour.js.map +1 -0
- package/dist/browser/pages/pkc-js-page-sorts/top-month.d.ts +3 -0
- package/dist/browser/pages/pkc-js-page-sorts/top-month.js +11 -0
- package/dist/browser/pages/pkc-js-page-sorts/top-month.js.map +1 -0
- package/dist/browser/pages/pkc-js-page-sorts/top-week.d.ts +3 -0
- package/dist/browser/pages/pkc-js-page-sorts/top-week.js +11 -0
- package/dist/browser/pages/pkc-js-page-sorts/top-week.js.map +1 -0
- package/dist/browser/pages/pkc-js-page-sorts/top-year.d.ts +3 -0
- package/dist/browser/pages/pkc-js-page-sorts/top-year.js +11 -0
- package/dist/browser/pages/pkc-js-page-sorts/top-year.js.map +1 -0
- package/dist/browser/pages/pkc-js-page-sorts/top.d.ts +3 -0
- package/dist/browser/pages/pkc-js-page-sorts/top.js +12 -0
- package/dist/browser/pages/pkc-js-page-sorts/top.js.map +1 -0
- package/dist/browser/pages/pkc-js-page-sorts/util.d.ts +3 -0
- package/dist/browser/pages/pkc-js-page-sorts/util.js +7 -0
- package/dist/browser/pages/pkc-js-page-sorts/util.js.map +1 -0
- package/dist/browser/pages/schema.d.ts +1 -0
- package/dist/browser/pages/schema.js +10 -3
- package/dist/browser/pages/schema.js.map +1 -1
- package/dist/browser/pages/types.d.ts +8 -0
- package/dist/browser/pages/util.d.ts +2 -1
- package/dist/browser/pages/util.js +11 -10
- package/dist/browser/pages/util.js.map +1 -1
- package/dist/browser/pkc/pkc.d.ts +2 -1
- package/dist/browser/pkc/pkc.js +2 -1
- package/dist/browser/pkc/pkc.js.map +1 -1
- package/dist/browser/pkc-error.d.ts +1 -1
- package/dist/browser/publications/comment/comment-client-manager.d.ts +2 -2
- package/dist/browser/publications/comment/comment-client-manager.js +3 -1
- package/dist/browser/publications/comment/comment-client-manager.js.map +1 -1
- package/dist/browser/publications/community-edit/schema.d.ts +64 -0
- package/dist/browser/pubsub-messages/schema.d.ts +32 -0
- package/dist/browser/rpc/src/index.js +14 -1
- package/dist/browser/rpc/src/index.js.map +1 -1
- package/dist/browser/rpc/src/lib/pkc-js/index.d.ts +559 -0
- package/dist/browser/rpc/src/schema.d.ts +3371 -0
- package/dist/browser/rpc/src/schema.js +6 -2
- package/dist/browser/rpc/src/schema.js.map +1 -1
- package/dist/browser/runtime/browser/community/page-sorts/index.d.ts +1 -0
- package/dist/browser/runtime/browser/community/page-sorts/index.js +6 -0
- package/dist/browser/runtime/browser/community/page-sorts/index.js.map +1 -0
- package/dist/browser/runtime/node/community/db-handler.d.ts +78 -20
- package/dist/browser/runtime/node/community/db-handler.js +1151 -734
- package/dist/browser/runtime/node/community/db-handler.js.map +1 -1
- package/dist/browser/runtime/node/community/db-row-parser.d.ts +13 -0
- package/dist/browser/runtime/node/community/db-row-parser.js +58 -1
- package/dist/browser/runtime/node/community/db-row-parser.js.map +1 -1
- package/dist/browser/runtime/node/community/local-community/cleanup.js +23 -10
- package/dist/browser/runtime/node/community/local-community/cleanup.js.map +1 -1
- package/dist/browser/runtime/node/community/local-community/comment-updates.d.ts +12 -2
- package/dist/browser/runtime/node/community/local-community/comment-updates.js +101 -70
- package/dist/browser/runtime/node/community/local-community/comment-updates.js.map +1 -1
- package/dist/browser/runtime/node/community/local-community/db-state.js +8 -0
- package/dist/browser/runtime/node/community/local-community/db-state.js.map +1 -1
- package/dist/browser/runtime/node/community/local-community/editing.d.ts +9 -2
- package/dist/browser/runtime/node/community/local-community/editing.js +32 -5
- package/dist/browser/runtime/node/community/local-community/editing.js.map +1 -1
- package/dist/browser/runtime/node/community/local-community/ipns-publishing.d.ts +2 -2
- package/dist/browser/runtime/node/community/local-community/ipns-publishing.js +49 -27
- package/dist/browser/runtime/node/community/local-community/ipns-publishing.js.map +1 -1
- package/dist/browser/runtime/node/community/local-community/lifecycle.d.ts +1 -0
- package/dist/browser/runtime/node/community/local-community/lifecycle.js +49 -18
- package/dist/browser/runtime/node/community/local-community/lifecycle.js.map +1 -1
- package/dist/browser/runtime/node/community/local-community.d.ts +5 -1
- package/dist/browser/runtime/node/community/local-community.js +13 -1
- package/dist/browser/runtime/node/community/local-community.js.map +1 -1
- package/dist/browser/runtime/node/community/page-generator.d.ts +44 -413
- package/dist/browser/runtime/node/community/page-generator.js +531 -308
- package/dist/browser/runtime/node/community/page-generator.js.map +1 -1
- package/dist/browser/runtime/node/community/page-sorts/index.d.ts +64 -0
- package/dist/browser/runtime/node/community/page-sorts/index.js +264 -0
- package/dist/browser/runtime/node/community/page-sorts/index.js.map +1 -0
- package/dist/browser/runtime/node/util.js +1 -1
- package/dist/browser/runtime/node/util.js.map +1 -1
- package/dist/browser/schema/schema-util.d.ts +142 -1
- package/dist/browser/schema/schema-util.js +11 -0
- package/dist/browser/schema/schema-util.js.map +1 -1
- package/dist/browser/schema/schema.js +1 -1
- package/dist/browser/schema/schema.js.map +1 -1
- package/dist/browser/schema.d.ts +4469 -0
- package/dist/browser/schema.js +2 -1
- package/dist/browser/schema.js.map +1 -1
- package/dist/browser/signer/signatures.d.ts +1 -1
- package/dist/browser/signer/signatures.js +45 -1
- package/dist/browser/signer/signatures.js.map +1 -1
- package/dist/browser/test/test-util.d.ts +63 -0
- package/dist/browser/test/test-util.js +66 -11
- package/dist/browser/test/test-util.js.map +1 -1
- package/dist/browser/util.d.ts +4 -0
- package/dist/browser/util.js +87 -0
- package/dist/browser/util.js.map +1 -1
- package/dist/browser/version.js +1 -1
- package/dist/bundled/bundle-manifest.json +235 -211
- package/dist/bundled/chunks/{helia-for-pkc-BCVI3rjp.js → helia-for-pkc-BFRxfgOR.js} +58 -34
- package/dist/bundled/chunks/{local-community-t5S6o-0o.js → local-community-B9vbrS8T.js} +1 -1
- package/dist/bundled/chunks/{local-community-CK7TUwwd.js → local-community-C8TU8XWc.js} +6719 -6055
- package/dist/bundled/chunks/{node-BpoCiJjL.js → node-BbSBdhh8.js} +1201 -15
- package/dist/bundled/chunks/{rpc-local-community-CR1TnLfm.js → rpc-local-community-BIyZJsOt.js} +13632 -14131
- package/dist/bundled/chunks/{util-bxE0EcD4.js → util-BJFQrwth.js} +1 -1
- package/dist/bundled/chunks/{schema-DWRg9_bS.js → util-CdbKTOCa.js} +751 -601
- package/dist/bundled/index.js +2 -2
- package/dist/bundled/rpc/src/index.js +22 -9
- package/dist/node/clients/base-client-manager.d.ts +1 -1
- package/dist/node/clients/base-client-manager.js +6 -2
- package/dist/node/clients/base-client-manager.js.map +1 -1
- package/dist/node/clients/rpc-client/rpc-schema-util.d.ts +16 -0
- package/dist/node/clients/rpc-client/schema.d.ts +16 -0
- package/dist/node/community/remote-community.d.ts +1 -0
- package/dist/node/community/remote-community.js +1 -0
- package/dist/node/community/remote-community.js.map +1 -1
- package/dist/node/community/rpc-local-community.js +13 -0
- package/dist/node/community/rpc-local-community.js.map +1 -1
- package/dist/node/community/schema.d.ts +1369 -1
- package/dist/node/community/schema.js +90 -4
- package/dist/node/community/schema.js.map +1 -1
- package/dist/node/community/types.d.ts +13 -2
- package/dist/node/constants.d.ts +1 -0
- package/dist/node/constants.js +6 -0
- package/dist/node/constants.js.map +1 -1
- package/dist/node/errors.d.ts +15 -1
- package/dist/node/errors.js +15 -0
- package/dist/node/errors.js.map +1 -1
- package/dist/node/generated-version.d.ts +1 -1
- package/dist/node/generated-version.js +1 -1
- package/dist/node/helia/helia-for-pkc.js +41 -4
- package/dist/node/helia/helia-for-pkc.js.map +1 -1
- package/dist/node/helia/util.js +4 -1
- package/dist/node/helia/util.js.map +1 -1
- package/dist/node/index-with-compile-cache.d.ts +559 -0
- package/dist/node/index.d.ts +1120 -0
- package/dist/node/index.js +5 -0
- package/dist/node/index.js.map +1 -1
- package/dist/node/logger.js +10 -1
- package/dist/node/logger.js.map +1 -1
- package/dist/node/object-key-util.d.ts +2 -0
- package/dist/node/object-key-util.js +12 -0
- package/dist/node/object-key-util.js.map +1 -0
- package/dist/node/pages/page-sort-options.d.ts +14 -0
- package/dist/node/pages/page-sort-options.js +92 -0
- package/dist/node/pages/page-sort-options.js.map +1 -0
- package/dist/node/pages/page-sort-scoring.d.ts +35 -0
- package/dist/node/pages/page-sort-scoring.js +124 -0
- package/dist/node/pages/page-sort-scoring.js.map +1 -0
- package/dist/node/pages/pages-client-manager.d.ts +1 -0
- package/dist/node/pages/pages-client-manager.js +16 -4
- package/dist/node/pages/pages-client-manager.js.map +1 -1
- package/dist/node/pages/pkc-js-page-sorts/active.d.ts +3 -0
- package/dist/node/pages/pkc-js-page-sorts/active.js +15 -0
- package/dist/node/pages/pkc-js-page-sorts/active.js.map +1 -0
- package/dist/node/pages/pkc-js-page-sorts/best.d.ts +3 -0
- package/dist/node/pages/pkc-js-page-sorts/best.js +10 -0
- package/dist/node/pages/pkc-js-page-sorts/best.js.map +1 -0
- package/dist/node/pages/pkc-js-page-sorts/controversial.d.ts +3 -0
- package/dist/node/pages/pkc-js-page-sorts/controversial.js +10 -0
- package/dist/node/pages/pkc-js-page-sorts/controversial.js.map +1 -0
- package/dist/node/pages/pkc-js-page-sorts/hot.d.ts +3 -0
- package/dist/node/pages/pkc-js-page-sorts/hot.js +10 -0
- package/dist/node/pages/pkc-js-page-sorts/hot.js.map +1 -0
- package/dist/node/pages/pkc-js-page-sorts/index.d.ts +2 -0
- package/dist/node/pages/pkc-js-page-sorts/index.js +37 -0
- package/dist/node/pages/pkc-js-page-sorts/index.js.map +1 -0
- package/dist/node/pages/pkc-js-page-sorts/new-flat.d.ts +3 -0
- package/dist/node/pages/pkc-js-page-sorts/new-flat.js +12 -0
- package/dist/node/pages/pkc-js-page-sorts/new-flat.js.map +1 -0
- package/dist/node/pages/pkc-js-page-sorts/new.d.ts +3 -0
- package/dist/node/pages/pkc-js-page-sorts/new.js +10 -0
- package/dist/node/pages/pkc-js-page-sorts/new.js.map +1 -0
- package/dist/node/pages/pkc-js-page-sorts/old-flat.d.ts +3 -0
- package/dist/node/pages/pkc-js-page-sorts/old-flat.js +12 -0
- package/dist/node/pages/pkc-js-page-sorts/old-flat.js.map +1 -0
- package/dist/node/pages/pkc-js-page-sorts/old.d.ts +3 -0
- package/dist/node/pages/pkc-js-page-sorts/old.js +10 -0
- package/dist/node/pages/pkc-js-page-sorts/old.js.map +1 -0
- package/dist/node/pages/pkc-js-page-sorts/top-all.d.ts +3 -0
- package/dist/node/pages/pkc-js-page-sorts/top-all.js +10 -0
- package/dist/node/pages/pkc-js-page-sorts/top-all.js.map +1 -0
- package/dist/node/pages/pkc-js-page-sorts/top-day.d.ts +3 -0
- package/dist/node/pages/pkc-js-page-sorts/top-day.js +11 -0
- package/dist/node/pages/pkc-js-page-sorts/top-day.js.map +1 -0
- package/dist/node/pages/pkc-js-page-sorts/top-hour.d.ts +3 -0
- package/dist/node/pages/pkc-js-page-sorts/top-hour.js +11 -0
- package/dist/node/pages/pkc-js-page-sorts/top-hour.js.map +1 -0
- package/dist/node/pages/pkc-js-page-sorts/top-month.d.ts +3 -0
- package/dist/node/pages/pkc-js-page-sorts/top-month.js +11 -0
- package/dist/node/pages/pkc-js-page-sorts/top-month.js.map +1 -0
- package/dist/node/pages/pkc-js-page-sorts/top-week.d.ts +3 -0
- package/dist/node/pages/pkc-js-page-sorts/top-week.js +11 -0
- package/dist/node/pages/pkc-js-page-sorts/top-week.js.map +1 -0
- package/dist/node/pages/pkc-js-page-sorts/top-year.d.ts +3 -0
- package/dist/node/pages/pkc-js-page-sorts/top-year.js +11 -0
- package/dist/node/pages/pkc-js-page-sorts/top-year.js.map +1 -0
- package/dist/node/pages/pkc-js-page-sorts/top.d.ts +3 -0
- package/dist/node/pages/pkc-js-page-sorts/top.js +12 -0
- package/dist/node/pages/pkc-js-page-sorts/top.js.map +1 -0
- package/dist/node/pages/pkc-js-page-sorts/util.d.ts +3 -0
- package/dist/node/pages/pkc-js-page-sorts/util.js +7 -0
- package/dist/node/pages/pkc-js-page-sorts/util.js.map +1 -0
- package/dist/node/pages/schema.d.ts +1 -0
- package/dist/node/pages/schema.js +10 -3
- package/dist/node/pages/schema.js.map +1 -1
- package/dist/node/pages/types.d.ts +8 -0
- package/dist/node/pages/util.d.ts +2 -1
- package/dist/node/pages/util.js +11 -10
- package/dist/node/pages/util.js.map +1 -1
- package/dist/node/pkc/pkc.d.ts +2 -1
- package/dist/node/pkc/pkc.js +2 -1
- package/dist/node/pkc/pkc.js.map +1 -1
- package/dist/node/pkc-error.d.ts +1 -1
- package/dist/node/publications/comment/comment-client-manager.d.ts +2 -2
- package/dist/node/publications/comment/comment-client-manager.js +3 -1
- package/dist/node/publications/comment/comment-client-manager.js.map +1 -1
- package/dist/node/publications/community-edit/schema.d.ts +64 -0
- package/dist/node/pubsub-messages/schema.d.ts +32 -0
- package/dist/node/rpc/src/index.js +14 -1
- package/dist/node/rpc/src/index.js.map +1 -1
- package/dist/node/rpc/src/lib/pkc-js/index.d.ts +559 -0
- package/dist/node/rpc/src/schema.d.ts +3371 -0
- package/dist/node/rpc/src/schema.js +6 -2
- package/dist/node/rpc/src/schema.js.map +1 -1
- package/dist/node/runtime/browser/community/page-sorts/index.d.ts +1 -0
- package/dist/node/runtime/browser/community/page-sorts/index.js +6 -0
- package/dist/node/runtime/browser/community/page-sorts/index.js.map +1 -0
- package/dist/node/runtime/node/community/db-handler.d.ts +78 -20
- package/dist/node/runtime/node/community/db-handler.js +1151 -734
- package/dist/node/runtime/node/community/db-handler.js.map +1 -1
- package/dist/node/runtime/node/community/db-row-parser.d.ts +13 -0
- package/dist/node/runtime/node/community/db-row-parser.js +58 -1
- package/dist/node/runtime/node/community/db-row-parser.js.map +1 -1
- package/dist/node/runtime/node/community/local-community/cleanup.js +23 -10
- package/dist/node/runtime/node/community/local-community/cleanup.js.map +1 -1
- package/dist/node/runtime/node/community/local-community/comment-updates.d.ts +12 -2
- package/dist/node/runtime/node/community/local-community/comment-updates.js +101 -70
- package/dist/node/runtime/node/community/local-community/comment-updates.js.map +1 -1
- package/dist/node/runtime/node/community/local-community/db-state.js +8 -0
- package/dist/node/runtime/node/community/local-community/db-state.js.map +1 -1
- package/dist/node/runtime/node/community/local-community/editing.d.ts +9 -2
- package/dist/node/runtime/node/community/local-community/editing.js +32 -5
- package/dist/node/runtime/node/community/local-community/editing.js.map +1 -1
- package/dist/node/runtime/node/community/local-community/ipns-publishing.d.ts +2 -2
- package/dist/node/runtime/node/community/local-community/ipns-publishing.js +49 -27
- package/dist/node/runtime/node/community/local-community/ipns-publishing.js.map +1 -1
- package/dist/node/runtime/node/community/local-community/lifecycle.d.ts +1 -0
- package/dist/node/runtime/node/community/local-community/lifecycle.js +49 -18
- package/dist/node/runtime/node/community/local-community/lifecycle.js.map +1 -1
- package/dist/node/runtime/node/community/local-community.d.ts +5 -1
- package/dist/node/runtime/node/community/local-community.js +13 -1
- package/dist/node/runtime/node/community/local-community.js.map +1 -1
- package/dist/node/runtime/node/community/page-generator.d.ts +44 -413
- package/dist/node/runtime/node/community/page-generator.js +531 -308
- package/dist/node/runtime/node/community/page-generator.js.map +1 -1
- package/dist/node/runtime/node/community/page-sorts/index.d.ts +64 -0
- package/dist/node/runtime/node/community/page-sorts/index.js +264 -0
- package/dist/node/runtime/node/community/page-sorts/index.js.map +1 -0
- package/dist/node/runtime/node/util.js +1 -1
- package/dist/node/runtime/node/util.js.map +1 -1
- package/dist/node/schema/schema-util.d.ts +142 -1
- package/dist/node/schema/schema-util.js +11 -0
- package/dist/node/schema/schema-util.js.map +1 -1
- package/dist/node/schema/schema.js +1 -1
- package/dist/node/schema/schema.js.map +1 -1
- package/dist/node/schema.d.ts +4469 -0
- package/dist/node/schema.js +2 -1
- package/dist/node/schema.js.map +1 -1
- package/dist/node/signer/signatures.d.ts +1 -1
- package/dist/node/signer/signatures.js +45 -1
- package/dist/node/signer/signatures.js.map +1 -1
- package/dist/node/test/test-util.d.ts +63 -0
- package/dist/node/test/test-util.js +66 -11
- package/dist/node/test/test-util.js.map +1 -1
- package/dist/node/util.d.ts +4 -0
- package/dist/node/util.js +87 -0
- package/dist/node/util.js.map +1 -1
- package/dist/node/version.js +1 -1
- package/package.json +2 -2
- package/dist/bundled/chunks/{src-Dz9wtvrK.js → src-B07ldGHS.js} +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getEquivalentCommunityAddresses, hideClassPrivateProps, isStringDomain, removeNullUndefinedValues, timestamp } from "../../../util.js";
|
|
1
|
+
import { getEquivalentCommunityAddresses, hideClassPrivateProps, isStringDomain, removeNullUndefinedValues, timestamp, withSortedKeys } from "../../../util.js";
|
|
2
2
|
import { PKCError } from "../../../pkc-error.js";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import assert from "assert";
|
|
@@ -22,10 +22,13 @@ import KeyvBetterSqlite3 from "./keyv-better-sqlite3.js";
|
|
|
22
22
|
import { STORAGE_KEYS } from "../../../constants.js";
|
|
23
23
|
import { CommentEditPubsubMessagePublicationSchema, CommentEditPubsubMessagePublicationWithFlexibleAuthorSchema } from "../../../publications/comment-edit/schema.js";
|
|
24
24
|
import { TIMEFRAMES_TO_SECONDS } from "../../../pages/util.js";
|
|
25
|
-
import { parseCommentEditsRow, parseCommentUpdateRow, parseCommentsTableRow,
|
|
25
|
+
import { parseCommentEditsRow, parseCommentUpdateRow, parseCommentsTableRow, parseVoteRow, parseCommentModerationRow, createPositionalCommentRowMapper } from "./db-row-parser.js";
|
|
26
26
|
import { ZodError } from "zod";
|
|
27
27
|
import { messages } from "../../../errors.js";
|
|
28
28
|
import { getAuthorNameFromWire } from "../../../publications/publication-author.js";
|
|
29
|
+
export const createCommunityAuthorMemo = () => ({ writeGeneration: -1, authors: new Map() });
|
|
30
|
+
// Per-connection temp table holding the rowids of the comments the update cycle has to recalculate (issue #355)
|
|
31
|
+
const COMMENTS_TO_UPDATE_TEMP_TABLE = "temp.commentsToUpdate";
|
|
29
32
|
const TABLES = Object.freeze({
|
|
30
33
|
COMMENTS: "comments",
|
|
31
34
|
COMMENT_UPDATES: "commentUpdates",
|
|
@@ -34,23 +37,38 @@ const TABLES = Object.freeze({
|
|
|
34
37
|
COMMENT_EDITS: "commentEdits",
|
|
35
38
|
PSEUDONYMITY_ALIASES: "pseudonymityAliases"
|
|
36
39
|
});
|
|
40
|
+
// The address an alias row's original public key derives to, or null when the key is malformed (kept, never looked up)
|
|
41
|
+
function deriveAliasOriginalAuthorSignerAddress(originalAuthorPublicKey) {
|
|
42
|
+
try {
|
|
43
|
+
return getPKCAddressFromPublicKeySync(originalAuthorPublicKey);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
37
49
|
export class DbHandler {
|
|
38
50
|
constructor(community) {
|
|
51
|
+
// Advanced by every write to the publication tables (comments, votes, edits, moderations, aliases, purges). The
|
|
52
|
+
// update cycle's author memo (CommunityAuthorMemo) is only reused while it stays the same.
|
|
53
|
+
this._publicationWriteGeneration = 0;
|
|
39
54
|
this._community = community;
|
|
40
55
|
this._transactionDepth = 0;
|
|
41
56
|
this._createdTables = false;
|
|
42
57
|
this._columnNamesByTable = {};
|
|
58
|
+
this._positionalMappers = new Map();
|
|
59
|
+
this._preparedStatements = new Map();
|
|
43
60
|
hideClassPrivateProps(this);
|
|
44
61
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
62
|
+
// Statements the update cycle runs per comment, compiled once per connection: preparing the SQL again on every
|
|
63
|
+
// call cost more than running some of them (issue #351). A cached statement keeps the mode (raw, pluck) its call
|
|
64
|
+
// site sets, so one SQL string belongs to one call site.
|
|
65
|
+
_prepareCached(sql) {
|
|
66
|
+
let statement = this._preparedStatements.get(sql);
|
|
67
|
+
if (!statement) {
|
|
68
|
+
statement = this._db.prepare(sql);
|
|
69
|
+
this._preparedStatements.set(sql, statement);
|
|
70
|
+
}
|
|
71
|
+
return statement;
|
|
54
72
|
}
|
|
55
73
|
_parseCommentsTableRow(row) {
|
|
56
74
|
const parsed = parseCommentsTableRow(row);
|
|
@@ -92,6 +110,7 @@ export class DbHandler {
|
|
|
92
110
|
const dbFilePath = this._dbConfig.filename;
|
|
93
111
|
if (!this._db || !this._db.open) {
|
|
94
112
|
this._columnNamesByTable = {}; // a reopened file may have been migrated by another process
|
|
113
|
+
this._preparedStatements.clear();
|
|
95
114
|
this._db = new Database(dbFilePath, { ...this._dbConfig, ...dbConfigOptions });
|
|
96
115
|
if (!this._db.readonly) {
|
|
97
116
|
this._db.pragma("journal_mode = WAL");
|
|
@@ -145,6 +164,7 @@ export class DbHandler {
|
|
|
145
164
|
}
|
|
146
165
|
destoryConnection() {
|
|
147
166
|
const log = Logger("pkc-js:local-community:dbHandler:destroyConnection");
|
|
167
|
+
this._preparedStatements.clear(); // statements belong to the connection
|
|
148
168
|
if (this._db && this._db.open) {
|
|
149
169
|
if (!this._db.readonly) {
|
|
150
170
|
this._db.exec("PRAGMA wal_checkpoint"); // write all wal to disk
|
|
@@ -272,7 +292,7 @@ export class DbHandler {
|
|
|
272
292
|
protocolVersion TEXT NOT NULL,
|
|
273
293
|
signature TEXT NOT NULL, -- JSON
|
|
274
294
|
author TEXT NULLABLE, -- JSON
|
|
275
|
-
replies TEXT NULLABLE, -- JSON
|
|
295
|
+
replies TEXT NULLABLE, -- JSON: CID refs per sort (DbRepliesSchema)
|
|
276
296
|
lastChildCid TEXT NULLABLE,
|
|
277
297
|
lastReplyTimestamp INTEGER NULLABLE,
|
|
278
298
|
postUpdatesBucket INTEGER NULLABLE,
|
|
@@ -281,6 +301,12 @@ export class DbHandler {
|
|
|
281
301
|
)
|
|
282
302
|
`);
|
|
283
303
|
}
|
|
304
|
+
// The comment tree lookups every page and update cycle runs: children of a comment (reply pages, stale_replies),
|
|
305
|
+
// every reply under a post (requireReplies sorts). Without them each is a full scan of the comments table.
|
|
306
|
+
_createCommentsIndexes() {
|
|
307
|
+
this._db.exec(`CREATE INDEX IF NOT EXISTS idx_comments_parentCid ON ${TABLES.COMMENTS}(parentCid)`);
|
|
308
|
+
this._db.exec(`CREATE INDEX IF NOT EXISTS idx_comments_postCid ON ${TABLES.COMMENTS}(postCid)`);
|
|
309
|
+
}
|
|
284
310
|
_createVotesTable(tableName) {
|
|
285
311
|
this._db.exec(`
|
|
286
312
|
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
@@ -343,12 +369,22 @@ export class DbHandler {
|
|
|
343
369
|
commentCid TEXT NOT NULL PRIMARY KEY UNIQUE REFERENCES ${TABLES.COMMENTS}(cid) ON DELETE CASCADE,
|
|
344
370
|
aliasPrivateKey TEXT NOT NULL,
|
|
345
371
|
originalAuthorPublicKey TEXT NOT NULL,
|
|
372
|
+
originalAuthorSignerAddress TEXT NULLABLE, -- derived from originalAuthorPublicKey at insert, so an author's aliases are an indexed lookup (issue #351)
|
|
346
373
|
originalAuthorName TEXT NULLABLE, -- the original author's name (e.g., user.eth) if they used one
|
|
347
374
|
mode TEXT NOT NULL CHECK(mode IN ('per-post', 'per-reply', 'per-author')),
|
|
348
375
|
insertedAt INTEGER NOT NULL
|
|
349
376
|
)
|
|
350
377
|
`);
|
|
351
378
|
}
|
|
379
|
+
// The lookups the update cycle runs per comment: an author's comments (author.community karma), an author's
|
|
380
|
+
// aliases, and a comment's edits and moderations (flags, reason, flairs, approval, the author's latest edit).
|
|
381
|
+
// Without them each is a full scan of its table (issue #351).
|
|
382
|
+
_createAuthorIndexes() {
|
|
383
|
+
this._db.exec(`CREATE INDEX IF NOT EXISTS idx_comments_authorSignerAddress ON ${TABLES.COMMENTS}(authorSignerAddress)`);
|
|
384
|
+
this._db.exec(`CREATE INDEX IF NOT EXISTS idx_pseudonymityAliases_originalAuthorSignerAddress ON ${TABLES.PSEUDONYMITY_ALIASES}(originalAuthorSignerAddress)`);
|
|
385
|
+
this._db.exec(`CREATE INDEX IF NOT EXISTS idx_commentEdits_commentCid ON ${TABLES.COMMENT_EDITS}(commentCid)`);
|
|
386
|
+
this._db.exec(`CREATE INDEX IF NOT EXISTS idx_commentModerations_commentCid ON ${TABLES.COMMENT_MODERATIONS}(commentCid)`);
|
|
387
|
+
}
|
|
352
388
|
getDbVersion() {
|
|
353
389
|
const result = this._db.pragma("user_version", { simple: true });
|
|
354
390
|
return Number(result);
|
|
@@ -415,6 +451,7 @@ export class DbHandler {
|
|
|
415
451
|
this._db.exec(`DROP TABLE IF EXISTS ${TABLES.COMMENT_UPDATES}`);
|
|
416
452
|
}
|
|
417
453
|
this._columnNamesByTable = {}; // the loop below rewrites every table's schema
|
|
454
|
+
this._preparedStatements.clear();
|
|
418
455
|
const createTableFunctions = [
|
|
419
456
|
this._createCommentsTable.bind(this),
|
|
420
457
|
this._createCommentUpdatesTable.bind(this),
|
|
@@ -441,6 +478,8 @@ export class DbHandler {
|
|
|
441
478
|
this._db.exec(`ALTER TABLE ${tempTableName} RENAME TO ${tableName}`);
|
|
442
479
|
}
|
|
443
480
|
}
|
|
481
|
+
this._createCommentsIndexes(); // idempotent; the migration loop above recreates the tables without them
|
|
482
|
+
this._createAuthorIndexes();
|
|
444
483
|
if (needToMigrate) {
|
|
445
484
|
await this._purgeCommentsWithInvalidSchemaOrSignature();
|
|
446
485
|
await this._purgeCommentEditsWithInvalidSchemaOrSignature();
|
|
@@ -488,6 +527,7 @@ export class DbHandler {
|
|
|
488
527
|
assert.equal(newDbVersion, env.DB_VERSION);
|
|
489
528
|
this._createdTables = true;
|
|
490
529
|
this._columnNamesByTable = {}; // every table now carries the latest schema
|
|
530
|
+
this._preparedStatements.clear();
|
|
491
531
|
if (needToMigrate)
|
|
492
532
|
log(`Created/migrated the tables to the latest (${newDbVersion}) version and saved to path`, this._dbConfig.filename);
|
|
493
533
|
if (backupDbPath)
|
|
@@ -679,6 +719,12 @@ export class DbHandler {
|
|
|
679
719
|
delete srcRecord["originalAuthorDomain"];
|
|
680
720
|
}
|
|
681
721
|
}
|
|
722
|
+
// The alias reverse-lookup column (v42 → v43, issue #351). After the v39 rename above, so a pre-v39 row's key
|
|
723
|
+
// is already under originalAuthorPublicKey when it is derived.
|
|
724
|
+
if (currentDbVersion < 43 &&
|
|
725
|
+
srcTable === TABLES.PSEUDONYMITY_ALIASES &&
|
|
726
|
+
typeof srcRecord["originalAuthorPublicKey"] === "string")
|
|
727
|
+
srcRecord["originalAuthorSignerAddress"] = deriveAliasOriginalAuthorSignerAddress(srcRecord["originalAuthorPublicKey"]);
|
|
682
728
|
// Prepare record for insertion (stringify JSONs, convert booleans)
|
|
683
729
|
const processedRecord = this._processRecordsForDbBeforeInsert([srcRecord])[0];
|
|
684
730
|
// Map values including rowid (preserve the original rowid value)
|
|
@@ -849,15 +895,18 @@ export class DbHandler {
|
|
|
849
895
|
}
|
|
850
896
|
}
|
|
851
897
|
deleteVote(authorSignerAddress, commentCid) {
|
|
898
|
+
this._publicationWriteGeneration++;
|
|
852
899
|
this._db
|
|
853
900
|
.prepare(`DELETE FROM ${TABLES.VOTES} WHERE commentCid = ? AND authorSignerAddress = ?`)
|
|
854
901
|
.run(commentCid, authorSignerAddress);
|
|
855
902
|
}
|
|
856
903
|
_deleteCommentEditRow(rowid) {
|
|
904
|
+
this._publicationWriteGeneration++;
|
|
857
905
|
const deleteResult = this._db.prepare(`DELETE FROM ${TABLES.COMMENT_EDITS} WHERE rowid = ?`).run(rowid);
|
|
858
906
|
return deleteResult.changes > 0;
|
|
859
907
|
}
|
|
860
908
|
insertVotes(votes) {
|
|
909
|
+
this._publicationWriteGeneration++;
|
|
861
910
|
if (votes.length === 0)
|
|
862
911
|
return;
|
|
863
912
|
const processedVotes = this._processRecordsForDbBeforeInsert(votes);
|
|
@@ -885,6 +934,7 @@ export class DbHandler {
|
|
|
885
934
|
insertMany(processedVotes);
|
|
886
935
|
}
|
|
887
936
|
insertComments(comments) {
|
|
937
|
+
this._publicationWriteGeneration++;
|
|
888
938
|
if (comments.length === 0)
|
|
889
939
|
return;
|
|
890
940
|
const processedComments = this._processRecordsForDbBeforeInsert(comments);
|
|
@@ -909,13 +959,17 @@ export class DbHandler {
|
|
|
909
959
|
insertMany(processedComments);
|
|
910
960
|
}
|
|
911
961
|
insertPseudonymityAliases(aliases) {
|
|
962
|
+
this._publicationWriteGeneration++;
|
|
912
963
|
if (aliases.length === 0)
|
|
913
964
|
return;
|
|
914
|
-
const processedAliases = this._processRecordsForDbBeforeInsert(aliases)
|
|
965
|
+
const processedAliases = this._processRecordsForDbBeforeInsert(aliases).map((alias) => ({
|
|
966
|
+
...alias,
|
|
967
|
+
originalAuthorSignerAddress: deriveAliasOriginalAuthorSignerAddress(alias.originalAuthorPublicKey)
|
|
968
|
+
}));
|
|
915
969
|
const stmt = this._db.prepare(`
|
|
916
970
|
INSERT OR REPLACE INTO ${TABLES.PSEUDONYMITY_ALIASES}
|
|
917
|
-
(commentCid, aliasPrivateKey, originalAuthorPublicKey, originalAuthorName, mode, insertedAt)
|
|
918
|
-
VALUES (@commentCid, @aliasPrivateKey, @originalAuthorPublicKey, @originalAuthorName, @mode, @insertedAt)
|
|
971
|
+
(commentCid, aliasPrivateKey, originalAuthorPublicKey, originalAuthorSignerAddress, originalAuthorName, mode, insertedAt)
|
|
972
|
+
VALUES (@commentCid, @aliasPrivateKey, @originalAuthorPublicKey, @originalAuthorSignerAddress, @originalAuthorName, @mode, @insertedAt)
|
|
919
973
|
`);
|
|
920
974
|
const insertMany = this._db.transaction((items) => {
|
|
921
975
|
for (const alias of items)
|
|
@@ -927,7 +981,7 @@ export class DbHandler {
|
|
|
927
981
|
const processedUpdates = this._processRecordsForDbBeforeInsert(updates);
|
|
928
982
|
// Get all column names from the comment_updates table to create defaults
|
|
929
983
|
const columnNames = this._getColumnNames(TABLES.COMMENT_UPDATES);
|
|
930
|
-
const stmt = this.
|
|
984
|
+
const stmt = this._prepareCached(`
|
|
931
985
|
INSERT INTO ${TABLES.COMMENT_UPDATES}
|
|
932
986
|
(cid, edit, upvoteCount, downvoteCount, replyCount, childCount, number, postNumber, flairs, spoiler, nsfw, pinned, locked, archived, removed, approved, reason, updatedAt, protocolVersion, signature, author, replies, lastChildCid, lastReplyTimestamp, postUpdatesBucket, publishedToPostUpdatesMFS, insertedAt)
|
|
933
987
|
VALUES (@cid, @edit, @upvoteCount, @downvoteCount, @replyCount, @childCount, @number, @postNumber, @flairs, @spoiler, @nsfw, @pinned, @locked, @archived, @removed, @approved, @reason, @updatedAt, @protocolVersion, @signature, @author, @replies, @lastChildCid, @lastReplyTimestamp, @postUpdatesBucket, @publishedToPostUpdatesMFS, @insertedAt)
|
|
@@ -954,6 +1008,7 @@ export class DbHandler {
|
|
|
954
1008
|
upsertMany(processedUpdates);
|
|
955
1009
|
}
|
|
956
1010
|
insertCommentModerations(moderations) {
|
|
1011
|
+
this._publicationWriteGeneration++;
|
|
957
1012
|
if (moderations.length === 0)
|
|
958
1013
|
return;
|
|
959
1014
|
const processedModerations = this._processRecordsForDbBeforeInsert(moderations);
|
|
@@ -976,6 +1031,7 @@ export class DbHandler {
|
|
|
976
1031
|
insertMany(processedModerations);
|
|
977
1032
|
}
|
|
978
1033
|
insertCommentEdits(edits) {
|
|
1034
|
+
this._publicationWriteGeneration++;
|
|
979
1035
|
if (edits.length === 0)
|
|
980
1036
|
return;
|
|
981
1037
|
const processedEdits = this._processRecordsForDbBeforeInsert(edits);
|
|
@@ -1083,346 +1139,255 @@ export class DbHandler {
|
|
|
1083
1139
|
whereClauses.push(this._approvedClause(commentUpdatesTable));
|
|
1084
1140
|
return { whereClauses, params };
|
|
1085
1141
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
SELECT MAX(timestamp) AS max_timestamp FROM descendants
|
|
1105
|
-
`;
|
|
1106
|
-
const result = this._db.prepare(query).get(comment.cid, ...addrParams);
|
|
1107
|
-
if (result.max_timestamp === null)
|
|
1108
|
-
return undefined;
|
|
1109
|
-
return result.max_timestamp;
|
|
1142
|
+
// The column set of a page entry: every CommentIpfs column plus extraProps, and every CommentUpdate column
|
|
1143
|
+
// unless the caller excludes some (a flat page drops `replies`). Compiled mappers are cached per column set.
|
|
1144
|
+
_pageEntryMapper(commentUpdateFieldsToExclude, existingOnly = false) {
|
|
1145
|
+
const commentUpdateCols = keys(commentUpdateFieldsToExclude ? omit(CommentUpdateSchema.shape, commentUpdateFieldsToExclude) : CommentUpdateSchema.shape);
|
|
1146
|
+
const commentIpfsCols = [...keys(CommentIpfsSchema.shape), "extraProps"];
|
|
1147
|
+
return this._positionalMapperFor({
|
|
1148
|
+
commentIpfsCols: existingOnly ? this._existingColumns(TABLES.COMMENTS, commentIpfsCols) : commentIpfsCols,
|
|
1149
|
+
commentUpdateCols: existingOnly ? this._existingColumns(TABLES.COMMENT_UPDATES, commentUpdateCols) : commentUpdateCols
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
_positionalMapperFor(cols) {
|
|
1153
|
+
const key = `${cols.commentIpfsCols.join(",")}|${cols.commentUpdateCols.join(",")}`;
|
|
1154
|
+
let mapper = this._positionalMappers.get(key);
|
|
1155
|
+
if (!mapper) {
|
|
1156
|
+
mapper = createPositionalCommentRowMapper(cols);
|
|
1157
|
+
this._positionalMappers.set(key, mapper);
|
|
1158
|
+
}
|
|
1159
|
+
return mapper;
|
|
1110
1160
|
}
|
|
1111
1161
|
queryPageComments(options) {
|
|
1112
|
-
const
|
|
1113
|
-
? omit(CommentUpdateSchema.shape, options.commentUpdateFieldsToExclude)
|
|
1114
|
-
: CommentUpdateSchema.shape);
|
|
1115
|
-
const commentUpdateSelects = commentUpdateCols.map((col) => `${TABLES.COMMENT_UPDATES}.${col} AS commentUpdate_${col}`);
|
|
1116
|
-
const commentIpfsCols = [...keys(CommentIpfsSchema.shape), "extraProps"];
|
|
1117
|
-
const commentIpfsSelects = commentIpfsCols.map((col) => `${TABLES.COMMENTS}.${col} AS commentIpfs_${col}`);
|
|
1162
|
+
const mapper = this._pageEntryMapper(options.commentUpdateFieldsToExclude);
|
|
1118
1163
|
const { whereClauses, params } = this._buildPageQueryParts(options);
|
|
1119
1164
|
const queryStr = `
|
|
1120
|
-
SELECT ${
|
|
1165
|
+
SELECT ${mapper.selectList((col) => `${TABLES.COMMENTS}.${col}`, (col) => `${TABLES.COMMENT_UPDATES}.${col}`)}
|
|
1121
1166
|
FROM ${TABLES.COMMENTS} INNER JOIN ${TABLES.COMMENT_UPDATES} ON ${TABLES.COMMENTS}.cid = ${TABLES.COMMENT_UPDATES}.cid
|
|
1122
1167
|
WHERE ${whereClauses.join(" AND ")}
|
|
1123
1168
|
`;
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
const commentIpfsCols = [...keys(CommentIpfsSchema.shape), "extraProps"];
|
|
1137
|
-
// Base case uses full table names (not aliases) to match _buildPageQueryParts WHERE clauses
|
|
1138
|
-
const baseCommentUpdateSelects = commentUpdateCols.map((col) => `${TABLES.COMMENT_UPDATES}.${col} AS commentUpdate_${col}`);
|
|
1139
|
-
const baseCommentIpfsSelects = commentIpfsCols.map((col) => `${TABLES.COMMENTS}.${col} AS commentIpfs_${col}`);
|
|
1140
|
-
// Recursive part uses aliases since it joins fresh tables
|
|
1141
|
-
const recCommentUpdateSelects = commentUpdateCols.map((col) => `cu2.${col} AS commentUpdate_${col}`);
|
|
1142
|
-
const recCommentIpfsSelects = commentIpfsCols.map((col) => `c2.${col} AS commentIpfs_${col}`);
|
|
1143
|
-
const { whereClauses, params } = this._buildPageQueryParts(options);
|
|
1144
|
-
// Build recursive filter clauses (same pattern as queryFlattenedPageReplies)
|
|
1145
|
-
const recursiveFilterClauses = [];
|
|
1146
|
-
if (options.excludeCommentsWithDifferentCommunityAddress) {
|
|
1147
|
-
const { clause, params: addrParams } = this._communityAddressClause("c2");
|
|
1148
|
-
recursiveFilterClauses.push(clause);
|
|
1149
|
-
params.push(...addrParams);
|
|
1150
|
-
}
|
|
1151
|
-
if (options.excludeCommentPendingApproval)
|
|
1152
|
-
recursiveFilterClauses.push(this._pendingApprovalClause("c2"));
|
|
1153
|
-
if (options.excludeRemovedComments)
|
|
1154
|
-
recursiveFilterClauses.push(this._removedClause("cu2"));
|
|
1155
|
-
if (options.excludeDeletedComments)
|
|
1156
|
-
recursiveFilterClauses.push(this._deletedFromLookupClause("d2"));
|
|
1157
|
-
if (options.excludeCommentWithApprovedFalse)
|
|
1158
|
-
recursiveFilterClauses.push(this._approvedClause("cu2"));
|
|
1159
|
-
const recursiveWhereExtra = recursiveFilterClauses.length > 0 ? `AND ${recursiveFilterClauses.join(" AND ")}` : "";
|
|
1160
|
-
const deletedLookupJoin = options.excludeDeletedComments
|
|
1161
|
-
? `LEFT JOIN (SELECT cid, json_extract(edit, '$.deleted') AS deleted_flag FROM ${TABLES.COMMENT_UPDATES}) AS d2 ON c2.cid = d2.cid`
|
|
1162
|
-
: "";
|
|
1163
|
-
const queryStr = `
|
|
1164
|
-
WITH RECURSIVE reply_tree AS (
|
|
1165
|
-
-- Base: direct children of the target comment
|
|
1166
|
-
SELECT ${baseCommentIpfsSelects.join(", ")}, ${baseCommentUpdateSelects.join(", ")},
|
|
1167
|
-
${TABLES.COMMENTS}.parentCid AS tree_parent
|
|
1168
|
-
FROM ${TABLES.COMMENTS}
|
|
1169
|
-
INNER JOIN ${TABLES.COMMENT_UPDATES} ON ${TABLES.COMMENTS}.cid = ${TABLES.COMMENT_UPDATES}.cid
|
|
1170
|
-
WHERE ${whereClauses.join(" AND ")}
|
|
1171
|
-
|
|
1172
|
-
UNION ALL
|
|
1173
|
-
|
|
1174
|
-
-- Recursive: follow commentCids from each parent's replies
|
|
1175
|
-
SELECT ${recCommentIpfsSelects.join(", ")}, ${recCommentUpdateSelects.join(", ")},
|
|
1176
|
-
rt.commentUpdate_cid AS tree_parent
|
|
1177
|
-
FROM reply_tree rt
|
|
1178
|
-
CROSS JOIN json_each(
|
|
1179
|
-
json_extract(rt.commentUpdate_replies, '$.best.commentCids')
|
|
1180
|
-
) child_ref
|
|
1181
|
-
JOIN ${TABLES.COMMENTS} c2 ON c2.cid = child_ref.value
|
|
1182
|
-
JOIN ${TABLES.COMMENT_UPDATES} cu2 ON c2.cid = cu2.cid
|
|
1183
|
-
${deletedLookupJoin}
|
|
1184
|
-
WHERE rt.commentUpdate_replies IS NOT NULL
|
|
1185
|
-
AND json_type(rt.commentUpdate_replies, '$.best.commentCids') = 'array'
|
|
1186
|
-
${recursiveWhereExtra}
|
|
1187
|
-
)
|
|
1188
|
-
SELECT * FROM reply_tree
|
|
1189
|
-
`;
|
|
1190
|
-
const rowsRaw = this._db.prepare(queryStr).all(...params);
|
|
1191
|
-
// Group by tree_parent to reconstruct the hierarchy
|
|
1169
|
+
const rows = this._prepareCached(queryStr)
|
|
1170
|
+
.raw(true)
|
|
1171
|
+
.all(...params);
|
|
1172
|
+
return rows.map(mapper.map);
|
|
1173
|
+
}
|
|
1174
|
+
// Nested reply trees out of page entries and the parent each was reached through (its own parentCid, whether
|
|
1175
|
+
// it came from a flat subtree read or from the level walk).
|
|
1176
|
+
// A child listed under two preloaded sorts of the same parent comes back twice and is kept once. `attachReplies`
|
|
1177
|
+
// rebuilds one page per preloaded sort of a parent, in that sort's commentCids order (the CTE's row order is not
|
|
1178
|
+
// the json_each order once the JOINs are involved), and `buildWireCommentUpdate` swaps the DB-format `replies`
|
|
1179
|
+
// for the resolved pages, or for `pageCids` alone when nothing is embedded but pages exist.
|
|
1180
|
+
_assembleReplyTrees(items) {
|
|
1192
1181
|
const parsedByCid = new Map();
|
|
1193
1182
|
const childrenByParent = new Map();
|
|
1194
|
-
for (const
|
|
1195
|
-
const
|
|
1196
|
-
parsedByCid.
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1183
|
+
for (const { entry, parent } of items) {
|
|
1184
|
+
const cid = entry.commentUpdate.cid;
|
|
1185
|
+
if (parsedByCid.has(cid))
|
|
1186
|
+
continue;
|
|
1187
|
+
parsedByCid.set(cid, entry);
|
|
1188
|
+
let siblings = childrenByParent.get(parent);
|
|
1189
|
+
if (!siblings)
|
|
1190
|
+
childrenByParent.set(parent, (siblings = new Map()));
|
|
1191
|
+
siblings.set(cid, entry);
|
|
1192
|
+
}
|
|
1193
|
+
// Keys stay sorted (see createPositionalCommentRowMapper): `replies` is re-inserted in order, and
|
|
1194
|
+
// `pageCids` sorts before `pages`
|
|
1203
1195
|
const buildWireCommentUpdate = (commentUpdate, resolvedReplies) => {
|
|
1204
1196
|
const { replies: dbReplies, ...rest } = commentUpdate;
|
|
1205
1197
|
if (resolvedReplies)
|
|
1206
|
-
return { ...rest, replies: resolvedReplies };
|
|
1207
|
-
// If no resolved inline replies but DB has allPageCids, reconstruct pageCids so clients can fetch pages
|
|
1198
|
+
return withSortedKeys({ ...rest, replies: resolvedReplies });
|
|
1208
1199
|
if (dbReplies) {
|
|
1209
|
-
const dbEntries = dbReplies;
|
|
1210
1200
|
const pageCids = {};
|
|
1211
|
-
for (const
|
|
1201
|
+
for (const sortName of Object.keys(dbReplies).sort()) {
|
|
1202
|
+
const sortEntry = dbReplies[sortName];
|
|
1212
1203
|
if (sortEntry?.allPageCids?.[0])
|
|
1213
1204
|
pageCids[sortName] = sortEntry.allPageCids[0];
|
|
1214
1205
|
}
|
|
1215
1206
|
if (Object.keys(pageCids).length > 0)
|
|
1216
|
-
return { ...rest, replies: { pages: {}
|
|
1207
|
+
return withSortedKeys({ ...rest, replies: { pageCids, pages: {} } });
|
|
1217
1208
|
}
|
|
1218
1209
|
return rest;
|
|
1219
1210
|
};
|
|
1220
|
-
// Recursively attach nested replies
|
|
1221
1211
|
const attachReplies = (cid) => {
|
|
1222
1212
|
const children = childrenByParent.get(cid);
|
|
1223
|
-
if (!children?.
|
|
1213
|
+
if (!children?.size)
|
|
1224
1214
|
return undefined;
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
best: {
|
|
1237
|
-
comments: orderedChildren.map((child) => ({
|
|
1215
|
+
const parentDbReplies = parsedByCid.get(cid)?.commentUpdate.replies;
|
|
1216
|
+
const pages = {};
|
|
1217
|
+
for (const sortName of Object.keys(parentDbReplies ?? {}).sort()) {
|
|
1218
|
+
const sortEntry = parentDbReplies[sortName];
|
|
1219
|
+
if (!sortEntry?.commentCids)
|
|
1220
|
+
continue;
|
|
1221
|
+
const comments = [];
|
|
1222
|
+
for (const childCid of sortEntry.commentCids) {
|
|
1223
|
+
const child = children.get(childCid);
|
|
1224
|
+
if (child)
|
|
1225
|
+
comments.push({
|
|
1238
1226
|
comment: child.comment,
|
|
1239
|
-
commentUpdate: buildWireCommentUpdate(child.commentUpdate, attachReplies(
|
|
1240
|
-
})
|
|
1241
|
-
}
|
|
1227
|
+
commentUpdate: buildWireCommentUpdate(child.commentUpdate, attachReplies(childCid))
|
|
1228
|
+
});
|
|
1242
1229
|
}
|
|
1243
|
-
|
|
1230
|
+
pages[sortName] = { comments };
|
|
1231
|
+
}
|
|
1232
|
+
if (Object.keys(pages).length === 0)
|
|
1233
|
+
return undefined;
|
|
1234
|
+
return { pages };
|
|
1244
1235
|
};
|
|
1245
|
-
|
|
1246
|
-
const directChildren = childrenByParent.get(options.parentCid) ?? [];
|
|
1247
|
-
return directChildren.map((child) => ({
|
|
1248
|
-
comment: child.comment,
|
|
1249
|
-
commentUpdate: buildWireCommentUpdate(child.commentUpdate, attachReplies(child.commentUpdate.cid))
|
|
1250
|
-
}));
|
|
1236
|
+
return { parsedByCid, childrenByParent, buildWireCommentUpdate, attachReplies };
|
|
1251
1237
|
}
|
|
1238
|
+
// The page entries under one comment: its direct children that pass the page's exclusions, each carrying the
|
|
1239
|
+
// reply pages its own CommentUpdate was signed with (resolveRepliesCidRefsForEntries). The exclusions apply to
|
|
1240
|
+
// the children only: what a child embeds was fixed when the child was signed, and a change below it re-flags the
|
|
1241
|
+
// child before its parent (stale_replies), so re-filtering the nested tree here could only disagree with the
|
|
1242
|
+
// child's signature. The recursive CTE this replaced cost a scan of the board per call (issue #351).
|
|
1243
|
+
queryPageCommentsWithResolvedReplies(options) {
|
|
1244
|
+
return this.resolveRepliesCidRefsForEntries(this.queryPageComments(options));
|
|
1245
|
+
}
|
|
1246
|
+
// Whether a loaded entry's CommentUpdate has replies to embed: its DB-format `replies` names a preloaded page or
|
|
1247
|
+
// a page CID (deriveDbReplies never stores an empty object)
|
|
1248
|
+
_entryHasDbReplies(entry) {
|
|
1249
|
+
const replies = entry.commentUpdate.replies;
|
|
1250
|
+
if (!replies)
|
|
1251
|
+
return false;
|
|
1252
|
+
return Object.values(replies).some((sortEntry) => sortEntry?.commentCids || sortEntry?.allPageCids);
|
|
1253
|
+
}
|
|
1254
|
+
// Run one statement per batch of cids, under the SQLite variable cap, collecting every row
|
|
1255
|
+
_forEachCidBatch(cids, queryFor, onRow, raw = false) {
|
|
1256
|
+
for (const { placeholders, params } of this._cidChunks(cids)) {
|
|
1257
|
+
const statement = this._prepareCached(queryFor(placeholders));
|
|
1258
|
+
if (raw)
|
|
1259
|
+
statement.raw(true);
|
|
1260
|
+
for (const row of statement.all(...params))
|
|
1261
|
+
onRow(row);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
// Every reply under the given posts (depth > 0, by postCid, indexed), as page entries with their DB-format reply
|
|
1265
|
+
// refs, in one flat read per batch of posts: what the page generator streams through, one batch of posts at a
|
|
1266
|
+
// time, to build those posts' nested reply pages (issue #351). Reads a post's whole subtree, listed or not; the
|
|
1267
|
+
// assembly keeps what the CID refs list.
|
|
1268
|
+
queryRepliesUnderPosts(postCids) {
|
|
1269
|
+
const mapper = this._pageEntryMapper(undefined, true);
|
|
1270
|
+
const entries = [];
|
|
1271
|
+
this._forEachCidBatch(postCids, (placeholders) => `
|
|
1272
|
+
SELECT ${mapper.selectList((col) => `c.${col}`, (col) => `cu.${col}`)}
|
|
1273
|
+
FROM ${TABLES.COMMENTS} c
|
|
1274
|
+
INNER JOIN ${TABLES.COMMENT_UPDATES} cu ON c.cid = cu.cid
|
|
1275
|
+
WHERE c.depth > 0 AND c.postCid IN (${placeholders})
|
|
1276
|
+
`, (row) => entries.push(mapper.map(row)), true);
|
|
1277
|
+
return entries;
|
|
1278
|
+
}
|
|
1279
|
+
// Resolve each entry's CID-ref replies into the wire form its CommentUpdate was signed with: one page per preloaded
|
|
1280
|
+
// sort in that sort's commentCids order, nested recursively, `pageCids` from allPageCids. Entries without replies
|
|
1281
|
+
// come back untouched. Posts are resolved from one flat read of their subtrees (queryRepliesUnderPosts); a set
|
|
1282
|
+
// that includes replies walks the listed CID refs level by level. The page generator calls this per bounded
|
|
1283
|
+
// batch of posts (issue #351), never for a whole board at once.
|
|
1252
1284
|
resolveRepliesCidRefsForEntries(entries) {
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
const
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
const queryStr = `
|
|
1279
|
-
WITH RECURSIVE reply_tree AS (
|
|
1280
|
-
SELECT ${commentIpfsSelects.join(", ")}, ${commentUpdateSelects.join(", ")},
|
|
1281
|
-
c.parentCid AS tree_parent
|
|
1282
|
-
FROM ${TABLES.COMMENTS} c
|
|
1283
|
-
INNER JOIN ${TABLES.COMMENT_UPDATES} cu ON c.cid = cu.cid
|
|
1284
|
-
WHERE c.cid IN (${placeholders})
|
|
1285
|
-
|
|
1286
|
-
UNION ALL
|
|
1287
|
-
|
|
1288
|
-
SELECT ${recCommentIpfsSelects.join(", ")}, ${recCommentUpdateSelects.join(", ")},
|
|
1289
|
-
rt.commentUpdate_cid AS tree_parent
|
|
1290
|
-
FROM reply_tree rt
|
|
1291
|
-
CROSS JOIN json_each(
|
|
1292
|
-
json_extract(rt.commentUpdate_replies, '$.best.commentCids')
|
|
1293
|
-
) child_ref
|
|
1294
|
-
JOIN ${TABLES.COMMENTS} c2 ON c2.cid = child_ref.value
|
|
1295
|
-
JOIN ${TABLES.COMMENT_UPDATES} cu2 ON c2.cid = cu2.cid
|
|
1296
|
-
WHERE rt.commentUpdate_replies IS NOT NULL
|
|
1297
|
-
AND json_type(rt.commentUpdate_replies, '$.best.commentCids') = 'array'
|
|
1298
|
-
)
|
|
1299
|
-
SELECT * FROM reply_tree
|
|
1300
|
-
`;
|
|
1301
|
-
const rowsRaw = this._db.prepare(queryStr).all(...allCids);
|
|
1302
|
-
// Build lookup: cid -> parsed entry
|
|
1303
|
-
for (const row of rowsRaw) {
|
|
1304
|
-
const { comment, commentUpdate } = this._parsePrefixedComment(row);
|
|
1305
|
-
parsedByCid.set(commentUpdate.cid, { comment, commentUpdate });
|
|
1306
|
-
const parent = row.tree_parent;
|
|
1307
|
-
if (!childrenByParent.has(parent))
|
|
1308
|
-
childrenByParent.set(parent, []);
|
|
1309
|
-
childrenByParent.get(parent).push({ comment, commentUpdate });
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
// Build a commentUpdate for wire format: strip DB replies, attach resolved replies or reconstruct pageCids
|
|
1313
|
-
const buildWireCommentUpdate = (commentUpdate, resolvedReplies) => {
|
|
1314
|
-
const { replies: dbReplies, ...rest } = commentUpdate;
|
|
1315
|
-
if (resolvedReplies)
|
|
1316
|
-
return { ...rest, replies: resolvedReplies };
|
|
1317
|
-
// If no resolved inline replies but DB has allPageCids, reconstruct pageCids so clients can fetch pages
|
|
1318
|
-
if (dbReplies) {
|
|
1319
|
-
const dbEntries = dbReplies;
|
|
1320
|
-
const pageCids = {};
|
|
1321
|
-
for (const [sortName, sortEntry] of Object.entries(dbEntries)) {
|
|
1322
|
-
if (sortEntry?.allPageCids?.[0])
|
|
1323
|
-
pageCids[sortName] = sortEntry.allPageCids[0];
|
|
1324
|
-
}
|
|
1325
|
-
if (Object.keys(pageCids).length > 0)
|
|
1326
|
-
return { ...rest, replies: { pages: {}, pageCids } };
|
|
1285
|
+
const withReplies = entries.filter((entry) => this._entryHasDbReplies(entry));
|
|
1286
|
+
if (withReplies.length === 0)
|
|
1287
|
+
return entries;
|
|
1288
|
+
const trees = withReplies.every((entry) => entry.comment.depth === 0)
|
|
1289
|
+
? this._assembleReplyTrees(this.queryRepliesUnderPosts(withReplies.map((entry) => entry.commentUpdate.cid)).map((entry) => ({
|
|
1290
|
+
entry,
|
|
1291
|
+
parent: entry.comment.parentCid
|
|
1292
|
+
})))
|
|
1293
|
+
: this._assembleReplyTreesFromCidRefs(withReplies);
|
|
1294
|
+
return this._rebuildEntriesFromTrees(entries, trees);
|
|
1295
|
+
}
|
|
1296
|
+
// The listed-subtree resolution for entries that are not all posts (a reply's page, at any depth): walk the CID
|
|
1297
|
+
// refs level by level, each level one primary-key read of the cids the level above lists, batched under the
|
|
1298
|
+
// SQLite variable cap. Reads exactly the tree the entries embed, so a reply's cost is its own subtree (issue #351).
|
|
1299
|
+
_assembleReplyTreesFromCidRefs(entries) {
|
|
1300
|
+
const mapper = this._pageEntryMapper(undefined, true);
|
|
1301
|
+
const listedChildren = (ofEntries) => {
|
|
1302
|
+
const cids = [];
|
|
1303
|
+
for (const entry of ofEntries) {
|
|
1304
|
+
const replies = entry.commentUpdate.replies;
|
|
1305
|
+
if (!replies)
|
|
1306
|
+
continue;
|
|
1307
|
+
for (const sortEntry of Object.values(replies))
|
|
1308
|
+
if (sortEntry?.commentCids)
|
|
1309
|
+
cids.push(...sortEntry.commentCids);
|
|
1327
1310
|
}
|
|
1328
|
-
return
|
|
1329
|
-
};
|
|
1330
|
-
const attachReplies = (cid) => {
|
|
1331
|
-
const children = childrenByParent.get(cid);
|
|
1332
|
-
if (!children?.length)
|
|
1333
|
-
return undefined;
|
|
1334
|
-
// Sort children by the parent's commentCids order.
|
|
1335
|
-
// SQLite's recursive CTE does not guarantee row order matches json_each array order
|
|
1336
|
-
// when additional JOINs are involved, so we must explicitly reorder.
|
|
1337
|
-
const parent = parsedByCid.get(cid);
|
|
1338
|
-
const parentDbReplies = parent?.commentUpdate?.replies;
|
|
1339
|
-
const cidOrder = parentDbReplies?.best?.commentCids;
|
|
1340
|
-
const orderedChildren = cidOrder
|
|
1341
|
-
? cidOrder.map((childCid) => children.find((c) => c.commentUpdate.cid === childCid)).filter(Boolean)
|
|
1342
|
-
: children;
|
|
1343
|
-
return {
|
|
1344
|
-
pages: {
|
|
1345
|
-
best: {
|
|
1346
|
-
comments: orderedChildren.map((child) => ({
|
|
1347
|
-
comment: child.comment,
|
|
1348
|
-
commentUpdate: buildWireCommentUpdate(child.commentUpdate, attachReplies(child.commentUpdate.cid))
|
|
1349
|
-
}))
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
};
|
|
1311
|
+
return cids;
|
|
1353
1312
|
};
|
|
1354
|
-
|
|
1313
|
+
const items = [];
|
|
1314
|
+
const seen = new Set();
|
|
1315
|
+
let frontier = listedChildren(entries);
|
|
1316
|
+
while (frontier.length > 0) {
|
|
1317
|
+
const level = [];
|
|
1318
|
+
this._forEachCidBatch(frontier.filter((cid) => !seen.has(cid)), (placeholders) => `
|
|
1319
|
+
SELECT ${mapper.selectList((col) => `c.${col}`, (col) => `cu.${col}`)}
|
|
1320
|
+
FROM ${TABLES.COMMENTS} c
|
|
1321
|
+
INNER JOIN ${TABLES.COMMENT_UPDATES} cu ON c.cid = cu.cid
|
|
1322
|
+
WHERE c.cid IN (${placeholders})
|
|
1323
|
+
`, (row) => level.push(mapper.map(row)), true);
|
|
1324
|
+
for (const entry of level) {
|
|
1325
|
+
seen.add(entry.commentUpdate.cid);
|
|
1326
|
+
items.push({ entry, parent: entry.comment.parentCid });
|
|
1327
|
+
}
|
|
1328
|
+
frontier = listedChildren(level);
|
|
1329
|
+
}
|
|
1330
|
+
return this._assembleReplyTrees(items);
|
|
1331
|
+
}
|
|
1332
|
+
// Each entry's pages rebuilt from the assembled trees in its own commentCids order
|
|
1333
|
+
_rebuildEntriesFromTrees(entries, { parsedByCid, buildWireCommentUpdate, attachReplies }) {
|
|
1355
1334
|
return entries.map((entry) => {
|
|
1356
|
-
|
|
1357
|
-
if (!replies)
|
|
1358
|
-
return entry;
|
|
1359
|
-
// Check if it's DB CID-ref format (has commentCids or allPageCids)
|
|
1360
|
-
const isDbFormat = Object.values(replies).some((s) => s?.commentCids || s?.allPageCids);
|
|
1361
|
-
if (!isDbFormat)
|
|
1335
|
+
if (!this._entryHasDbReplies(entry))
|
|
1362
1336
|
return entry;
|
|
1363
|
-
|
|
1337
|
+
const replies = entry.commentUpdate.replies;
|
|
1364
1338
|
const resolvedPages = {};
|
|
1365
1339
|
const resolvedPageCids = {};
|
|
1366
|
-
for (const
|
|
1340
|
+
for (const sortName of Object.keys(replies).sort()) {
|
|
1341
|
+
const sortEntry = replies[sortName];
|
|
1367
1342
|
if (sortEntry?.commentCids) {
|
|
1368
|
-
const resolvedComments =
|
|
1369
|
-
|
|
1370
|
-
.
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1343
|
+
const resolvedComments = [];
|
|
1344
|
+
for (const cid of sortEntry.commentCids) {
|
|
1345
|
+
const child = parsedByCid.get(cid);
|
|
1346
|
+
if (child)
|
|
1347
|
+
resolvedComments.push({
|
|
1348
|
+
comment: child.comment,
|
|
1349
|
+
commentUpdate: buildWireCommentUpdate(child.commentUpdate, attachReplies(cid))
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
// nextCid from allPageCids[0]; the legacy nextCid field for old DB rows
|
|
1376
1353
|
const nextCidForSort = sortEntry.allPageCids?.[0] ?? sortEntry.nextCid;
|
|
1377
|
-
resolvedPages[sortName] = {
|
|
1378
|
-
comments: resolvedComments,
|
|
1379
|
-
...(nextCidForSort ? { nextCid: nextCidForSort } : {})
|
|
1380
|
-
};
|
|
1354
|
+
resolvedPages[sortName] = { comments: resolvedComments, ...(nextCidForSort ? { nextCid: nextCidForSort } : {}) };
|
|
1381
1355
|
}
|
|
1382
|
-
|
|
1383
|
-
if (sortEntry?.allPageCids?.[0]) {
|
|
1356
|
+
if (sortEntry?.allPageCids?.[0])
|
|
1384
1357
|
resolvedPageCids[sortName] = sortEntry.allPageCids[0];
|
|
1385
|
-
}
|
|
1386
1358
|
}
|
|
1387
1359
|
const { replies: _dbReplies, ...entryCommentUpdateRest } = entry.commentUpdate;
|
|
1388
1360
|
return {
|
|
1389
1361
|
...entry,
|
|
1390
|
-
commentUpdate: {
|
|
1362
|
+
commentUpdate: withSortedKeys({
|
|
1391
1363
|
...entryCommentUpdateRest,
|
|
1392
1364
|
replies: {
|
|
1393
|
-
|
|
1394
|
-
|
|
1365
|
+
...(Object.keys(resolvedPageCids).length > 0 ? { pageCids: resolvedPageCids } : {}),
|
|
1366
|
+
pages: resolvedPages
|
|
1395
1367
|
}
|
|
1396
|
-
}
|
|
1368
|
+
})
|
|
1397
1369
|
};
|
|
1398
1370
|
});
|
|
1399
1371
|
}
|
|
1400
1372
|
queryCommentAndCommentUpdateByCids(cids, opts) {
|
|
1401
1373
|
if (cids.length === 0)
|
|
1402
1374
|
return [];
|
|
1403
|
-
const
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1375
|
+
const mapper = this._positionalMapperFor({
|
|
1376
|
+
commentIpfsCols: this._existingColumns(TABLES.COMMENTS, opts.commentIpfsCols),
|
|
1377
|
+
commentUpdateCols: this._existingColumns(TABLES.COMMENT_UPDATES, opts.commentUpdateCols)
|
|
1378
|
+
});
|
|
1379
|
+
const entries = [];
|
|
1380
|
+
this._forEachCidBatch(cids, (placeholders) => `
|
|
1381
|
+
SELECT ${mapper.selectList((col) => `c.${col}`, (col) => `cu.${col}`)}
|
|
1408
1382
|
FROM ${TABLES.COMMENTS} c
|
|
1409
1383
|
INNER JOIN ${TABLES.COMMENT_UPDATES} cu ON c.cid = cu.cid
|
|
1410
1384
|
WHERE c.cid IN (${placeholders})
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
return rows.map((row) => {
|
|
1414
|
-
const { comment, commentUpdate } = this._parsePrefixedComment(row);
|
|
1415
|
-
return { comment, commentUpdate };
|
|
1416
|
-
});
|
|
1385
|
+
`, (row) => entries.push(mapper.map(row)), true);
|
|
1386
|
+
return entries;
|
|
1417
1387
|
}
|
|
1418
1388
|
queryFlattenedPageReplies(options) {
|
|
1419
|
-
const
|
|
1420
|
-
|
|
1421
|
-
: CommentUpdateSchema.shape);
|
|
1422
|
-
// TODO, is it omitting replies?
|
|
1423
|
-
const commentUpdateSelects = commentUpdateCols.map((col) => `c_updates.${col} AS commentUpdate_${col}`);
|
|
1424
|
-
const commentIpfsCols = [...keys(CommentIpfsSchema.shape), "extraProps"];
|
|
1425
|
-
const commentIpfsSelects = commentIpfsCols.map((col) => `comments_alias.${col} AS commentIpfs_${col}`);
|
|
1389
|
+
const mapper = this._pageEntryMapper(options.commentUpdateFieldsToExclude);
|
|
1390
|
+
const commentUpdateCols = mapper.commentUpdateCols;
|
|
1426
1391
|
let baseWhereClausesStr = "";
|
|
1427
1392
|
let recursiveWhereClausesStr = "";
|
|
1428
1393
|
const params = [options.parentCid];
|
|
@@ -1476,25 +1441,22 @@ export class DbHandler {
|
|
|
1476
1441
|
INNER JOIN comment_tree tree ON comments.parentCid = tree.cid
|
|
1477
1442
|
WHERE 1=1 ${recursiveWhereClausesStr}
|
|
1478
1443
|
)
|
|
1479
|
-
SELECT ${
|
|
1444
|
+
SELECT ${mapper.selectList((col) => `comments_alias.${col}`, (col) => `comments_alias.c_updates_${col}`)}
|
|
1480
1445
|
FROM comment_tree comments_alias
|
|
1481
1446
|
`;
|
|
1482
|
-
const
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
});
|
|
1447
|
+
const rows = this._prepareCached(query)
|
|
1448
|
+
.raw(true)
|
|
1449
|
+
.all(...params);
|
|
1450
|
+
return rows.map(mapper.map);
|
|
1487
1451
|
}
|
|
1488
1452
|
queryStoredCommentUpdate(comment) {
|
|
1489
|
-
const row = this.
|
|
1453
|
+
const row = this._prepareCached(`SELECT * FROM ${TABLES.COMMENT_UPDATES} WHERE cid = ?`).get(comment.cid);
|
|
1490
1454
|
if (!row)
|
|
1491
1455
|
return undefined;
|
|
1492
1456
|
return this._parseCommentUpdatesRow(row);
|
|
1493
1457
|
}
|
|
1494
1458
|
queryCommentUpdateTimestampBucketReplies(opts) {
|
|
1495
|
-
const row = this.
|
|
1496
|
-
.prepare(`SELECT updatedAt, postUpdatesBucket, replies FROM ${TABLES.COMMENT_UPDATES} WHERE cid = ?`)
|
|
1497
|
-
.get(opts.cid);
|
|
1459
|
+
const row = this._prepareCached(`SELECT updatedAt, postUpdatesBucket, replies FROM ${TABLES.COMMENT_UPDATES} WHERE cid = ?`).get(opts.cid);
|
|
1498
1460
|
if (!row)
|
|
1499
1461
|
return undefined;
|
|
1500
1462
|
return {
|
|
@@ -1504,7 +1466,7 @@ export class DbHandler {
|
|
|
1504
1466
|
};
|
|
1505
1467
|
}
|
|
1506
1468
|
queryCommentUpdateBucketAndReplies(opts) {
|
|
1507
|
-
const row = this.
|
|
1469
|
+
const row = this._prepareCached(`SELECT postUpdatesBucket, replies FROM ${TABLES.COMMENT_UPDATES} WHERE cid = ?`).get(opts.cid);
|
|
1508
1470
|
if (!row)
|
|
1509
1471
|
return undefined;
|
|
1510
1472
|
return {
|
|
@@ -1522,12 +1484,10 @@ export class DbHandler {
|
|
|
1522
1484
|
return row !== undefined;
|
|
1523
1485
|
}
|
|
1524
1486
|
queryCommentBySignatureEncoded(signatureEncoded) {
|
|
1525
|
-
const row = this.
|
|
1526
|
-
.prepare(`SELECT * FROM ${TABLES.COMMENTS}
|
|
1487
|
+
const row = this._prepareCached(`SELECT * FROM ${TABLES.COMMENTS}
|
|
1527
1488
|
WHERE json_extract(signature, '$.signature') = ?
|
|
1528
1489
|
OR originalCommentSignatureEncoded = ?
|
|
1529
|
-
LIMIT 1`)
|
|
1530
|
-
.get(signatureEncoded, signatureEncoded);
|
|
1490
|
+
LIMIT 1`).get(signatureEncoded, signatureEncoded);
|
|
1531
1491
|
if (!row)
|
|
1532
1492
|
return undefined;
|
|
1533
1493
|
return this._parseCommentsTableRow(row);
|
|
@@ -1557,132 +1517,236 @@ export class DbHandler {
|
|
|
1557
1517
|
return this._db.prepare(query).all(rootComment.parentCid);
|
|
1558
1518
|
}
|
|
1559
1519
|
queryCommentsPendingApproval() {
|
|
1560
|
-
const results = this.
|
|
1561
|
-
.prepare(`SELECT * FROM ${TABLES.COMMENTS} WHERE pendingApproval = 1 ORDER BY rowid DESC`)
|
|
1562
|
-
.all();
|
|
1520
|
+
const results = this._prepareCached(`SELECT * FROM ${TABLES.COMMENTS} WHERE pendingApproval = 1 ORDER BY rowid DESC`).all();
|
|
1563
1521
|
return results.map((r) => this._parseCommentsTableRow(r));
|
|
1564
1522
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1523
|
+
// Windowed reply sorts (settings.pages.replies[].options.maxAge) need a time-based trigger that nothing else
|
|
1524
|
+
// provides: a page's membership changes when a reply crosses the window boundary even though no row changed.
|
|
1525
|
+
// One arm per distinct window flags a parent only when a reply was inside the window at the parent's last
|
|
1526
|
+
// generation (cu.updatedAt) and is outside it now, so a quiet thread costs a range scan and nothing else.
|
|
1527
|
+
// Flat sorts filter the whole flattened subtree and are generated for posts only, so their arm walks
|
|
1528
|
+
// descendants up to the depth-0 ancestor. Pinned replies bypass the filter when pinnedFirst is true. Omitted
|
|
1529
|
+
// entirely (empty strings) when no reply sort is windowed, so the default config pays nothing.
|
|
1530
|
+
_buildWindowedReplySortArms(now) {
|
|
1531
|
+
const windowedSorts = (this._community._pageSorts?.replies ?? []).filter((sort) => typeof sort.maxAgeSeconds === "number");
|
|
1532
|
+
if (windowedSorts.length === 0)
|
|
1533
|
+
return { ctes: "", selects: [], params: {} };
|
|
1534
|
+
const ctes = [];
|
|
1535
|
+
const selects = [];
|
|
1536
|
+
const params = { windowNow: now };
|
|
1537
|
+
const seen = new Set();
|
|
1538
|
+
for (const sort of windowedSorts) {
|
|
1539
|
+
const key = `${sort.maxAgeSeconds}:${sort.flat}:${sort.pinnedFirst}`;
|
|
1540
|
+
if (seen.has(key))
|
|
1541
|
+
continue;
|
|
1542
|
+
seen.add(key);
|
|
1543
|
+
const i = ctes.length;
|
|
1544
|
+
params[`windowMaxAge${i}`] = sort.maxAgeSeconds;
|
|
1545
|
+
const pinnedClause = sort.pinnedFirst ? `AND (rcu.pinned IS NULL OR rcu.pinned != 1)` : "";
|
|
1546
|
+
const crossingClause = `r_ts >= cu_anc.updatedAt - :windowMaxAge${i} AND r_ts < :windowNow - :windowMaxAge${i}`;
|
|
1547
|
+
if (sort.flat)
|
|
1548
|
+
ctes.push(`
|
|
1549
|
+
windowed_flat_${i}_ancestors AS (
|
|
1550
|
+
SELECT r.cid AS reply_cid, r.timestamp AS r_ts, r.parentCid AS anc_cid FROM ${TABLES.COMMENTS} r WHERE r.parentCid IS NOT NULL
|
|
1551
|
+
UNION ALL
|
|
1552
|
+
SELECT wa.reply_cid, wa.r_ts, p.parentCid FROM windowed_flat_${i}_ancestors wa
|
|
1553
|
+
JOIN ${TABLES.COMMENTS} p ON p.cid = wa.anc_cid WHERE p.parentCid IS NOT NULL
|
|
1583
1554
|
),
|
|
1584
|
-
|
|
1585
|
-
SELECT
|
|
1586
|
-
|
|
1587
|
-
|
|
1555
|
+
windowed_${i} AS (
|
|
1556
|
+
SELECT DISTINCT wa.anc_cid AS cid
|
|
1557
|
+
FROM windowed_flat_${i}_ancestors wa
|
|
1558
|
+
JOIN ${TABLES.COMMENTS} anc ON anc.cid = wa.anc_cid AND anc.depth = 0
|
|
1559
|
+
JOIN ${TABLES.COMMENT_UPDATES} cu_anc ON cu_anc.cid = wa.anc_cid
|
|
1560
|
+
LEFT JOIN ${TABLES.COMMENT_UPDATES} rcu ON rcu.cid = wa.reply_cid
|
|
1561
|
+
WHERE ${crossingClause} ${pinnedClause}
|
|
1562
|
+
)`);
|
|
1563
|
+
else
|
|
1564
|
+
ctes.push(`
|
|
1565
|
+
windowed_${i} AS (
|
|
1566
|
+
SELECT DISTINCT r.parentCid AS cid
|
|
1567
|
+
FROM (SELECT cid, parentCid, timestamp AS r_ts FROM ${TABLES.COMMENTS} WHERE parentCid IS NOT NULL) r
|
|
1568
|
+
JOIN ${TABLES.COMMENT_UPDATES} cu_anc ON cu_anc.cid = r.parentCid
|
|
1569
|
+
LEFT JOIN ${TABLES.COMMENT_UPDATES} rcu ON rcu.cid = r.cid
|
|
1570
|
+
WHERE ${crossingClause} ${pinnedClause}
|
|
1571
|
+
)`);
|
|
1572
|
+
selects.push(`SELECT c.rowid FROM ${TABLES.COMMENTS} c JOIN windowed_${i} w${i} ON c.cid = w${i}.cid`);
|
|
1573
|
+
}
|
|
1574
|
+
return { ctes: ctes.join(","), selects, params };
|
|
1575
|
+
}
|
|
1576
|
+
// The rowids of every comment the update cycle has to recalculate, collected into a temp table (issue #355).
|
|
1577
|
+
//
|
|
1578
|
+
// The flag set used to be one query: recursive CTEs stacked over `SELECT c.*`, whose every UNION and
|
|
1579
|
+
// DISTINCT deduplicated ~900-byte rows on a temp B-tree and whose every level re-copied the board. On a
|
|
1580
|
+
// 3.1M-comment board that was 2.7 GB of heap and 5.7 minutes before the first CommentUpdate. Here the set
|
|
1581
|
+
// is an integer-keyed temp table, each arm of the old query is one INSERT OR IGNORE into it, and the two
|
|
1582
|
+
// cascades (an author's other comments, and the parent chain) read the table they extend. Integer keys are
|
|
1583
|
+
// what make it cheap: the same sequence keyed by cid strings took three times as long. Measured on that
|
|
1584
|
+
// board: 14.5 s for the whole sequence in the SQLite shell.
|
|
1585
|
+
//
|
|
1586
|
+
// The arms, in order, and what each stands for:
|
|
1587
|
+
// 1. no CommentUpdate row yet, or one not yet published to the postUpdates directory;
|
|
1588
|
+
// 2. a vote, edit, moderation or child comment inserted since the row was calculated (issue #209: the
|
|
1589
|
+
// row's insertedAt is the batch's start timestamp, so nothing inserted during a batch is missed);
|
|
1590
|
+
// 3. childCount no longer matches the counted children (a child removed, deleted or purged);
|
|
1591
|
+
// 4. lastChildCid no longer matches the newest counted child;
|
|
1592
|
+
// 5. a child listed in the stored replies pages has a newer CommentUpdate than the parent's;
|
|
1593
|
+
// 6. windowed reply sorts, one arm per distinct window (see _buildWindowedReplySortArms);
|
|
1594
|
+
// 7. every other comment by an author of a flagged comment (author.community aggregates);
|
|
1595
|
+
// 8. every ancestor of a flagged comment (its counts, last reply and reply pages read the child's row).
|
|
1596
|
+
// Pending-approval comments are never flagged and stop the parent-chain walk.
|
|
1597
|
+
// Make sure the tests in commentsToUpdate.db.community.test.ts are passing after changing any of them.
|
|
1598
|
+
_collectCommentsToBeUpdated() {
|
|
1599
|
+
const windowed = this._buildWindowedReplySortArms(timestamp());
|
|
1600
|
+
const notPending = (alias) => `(${alias}.pendingApproval IS NULL OR ${alias}.pendingApproval != 1)`;
|
|
1601
|
+
// A child that counts towards its parent's childCount: has a CommentUpdate, not removed, not deleted by its author
|
|
1602
|
+
const countedChild = `
|
|
1588
1603
|
FROM ${TABLES.COMMENTS} c
|
|
1589
1604
|
JOIN ${TABLES.COMMENT_UPDATES} cu_child ON c.cid = cu_child.cid
|
|
1590
|
-
LEFT JOIN (
|
|
1591
|
-
SELECT cid, json_extract(edit, '$.deleted') AS deleted_flag FROM ${TABLES.COMMENT_UPDATES}
|
|
1592
|
-
) deleted_lookup ON deleted_lookup.cid = c.cid
|
|
1593
1605
|
WHERE c.parentCid IS NOT NULL
|
|
1594
|
-
AND (c
|
|
1606
|
+
AND ${notPending("c")}
|
|
1595
1607
|
AND (cu_child.removed IS NOT 1 AND cu_child.removed IS NOT TRUE)
|
|
1596
|
-
AND (
|
|
1608
|
+
AND (json_extract(cu_child.edit, '$.deleted') IS NULL OR json_extract(cu_child.edit, '$.deleted') != 1)`;
|
|
1609
|
+
const insert = `INSERT OR IGNORE INTO ${COMMENTS_TO_UPDATE_TEMP_TABLE}`;
|
|
1610
|
+
const arms = [
|
|
1611
|
+
`${insert}
|
|
1612
|
+
SELECT c.rowid FROM ${TABLES.COMMENTS} c LEFT JOIN ${TABLES.COMMENT_UPDATES} cu ON c.cid = cu.cid
|
|
1613
|
+
WHERE ${notPending("c")}
|
|
1614
|
+
AND (cu.cid IS NULL OR (cu.publishedToPostUpdatesMFS = 0 OR cu.publishedToPostUpdatesMFS IS FALSE))`,
|
|
1615
|
+
`${insert}
|
|
1616
|
+
SELECT c.rowid FROM ${TABLES.COMMENTS} c JOIN ${TABLES.COMMENT_UPDATES} cu ON c.cid = cu.cid
|
|
1617
|
+
WHERE ${notPending("c")}
|
|
1618
|
+
AND (
|
|
1619
|
+
EXISTS (SELECT 1 FROM ${TABLES.VOTES} v WHERE v.commentCid = c.cid AND v.insertedAt >= cu.insertedAt)
|
|
1620
|
+
OR EXISTS (SELECT 1 FROM ${TABLES.COMMENT_EDITS} ce WHERE ce.commentCid = c.cid AND ce.insertedAt >= cu.insertedAt)
|
|
1621
|
+
OR EXISTS (SELECT 1 FROM ${TABLES.COMMENT_MODERATIONS} cm WHERE cm.commentCid = c.cid AND cm.insertedAt >= cu.insertedAt)
|
|
1622
|
+
OR EXISTS (SELECT 1 FROM ${TABLES.COMMENTS} cc WHERE cc.parentCid = c.cid AND cc.insertedAt >= cu.insertedAt)
|
|
1623
|
+
)`,
|
|
1624
|
+
`${insert}
|
|
1625
|
+
WITH child_counts AS (
|
|
1626
|
+
SELECT c.parentCid AS cid, COUNT(*) AS actual_child_count
|
|
1627
|
+
${countedChild}
|
|
1597
1628
|
GROUP BY c.parentCid
|
|
1598
|
-
)
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
WHERE c.parentCid IS NOT NULL
|
|
1610
|
-
AND (c.pendingApproval IS NULL OR c.pendingApproval != 1)
|
|
1611
|
-
AND (cu_child.removed IS NOT 1 AND cu_child.removed IS NOT TRUE)
|
|
1612
|
-
AND (deleted_lookup.deleted_flag IS NULL OR deleted_lookup.deleted_flag != 1)
|
|
1629
|
+
)
|
|
1630
|
+
SELECT parent.rowid
|
|
1631
|
+
FROM ${TABLES.COMMENT_UPDATES} cu_parent
|
|
1632
|
+
JOIN ${TABLES.COMMENTS} parent ON parent.cid = cu_parent.cid
|
|
1633
|
+
LEFT JOIN child_counts cc ON cc.cid = cu_parent.cid
|
|
1634
|
+
WHERE ${notPending("parent")}
|
|
1635
|
+
AND COALESCE(cc.actual_child_count, 0) != COALESCE(cu_parent.childCount, 0)`,
|
|
1636
|
+
`${insert}
|
|
1637
|
+
WITH last_child_rowids AS (
|
|
1638
|
+
SELECT c.parentCid AS cid, MAX(c.rowid) AS last_child_rowid
|
|
1639
|
+
${countedChild}
|
|
1613
1640
|
AND COALESCE(cu_child.approved, 1) != 0
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
)
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
base_updates AS (
|
|
1656
|
-
SELECT * FROM direct_updates
|
|
1657
|
-
UNION SELECT c.* FROM ${TABLES.COMMENTS} c JOIN stale_child_counts scc ON c.cid = scc.cid
|
|
1658
|
-
UNION SELECT c.* FROM ${TABLES.COMMENTS} c JOIN stale_last_child_cids slc ON c.cid = slc.cid
|
|
1659
|
-
UNION SELECT c.* FROM ${TABLES.COMMENTS} c JOIN stale_replies sr ON c.cid = sr.cid
|
|
1660
|
-
),
|
|
1661
|
-
authors_to_update AS (SELECT DISTINCT authorSignerAddress FROM base_updates),
|
|
1662
|
-
author_comments AS (
|
|
1663
|
-
SELECT c.* FROM ${TABLES.COMMENTS} c JOIN authors_to_update a ON c.authorSignerAddress = a.authorSignerAddress
|
|
1664
|
-
WHERE (c.pendingApproval IS NULL OR c.pendingApproval != 1)
|
|
1665
|
-
),
|
|
1666
|
-
comments_needing_update AS (
|
|
1667
|
-
SELECT * FROM base_updates
|
|
1668
|
-
UNION SELECT * FROM author_comments
|
|
1669
|
-
),
|
|
1670
|
-
parent_chain AS (
|
|
1671
|
-
SELECT DISTINCT p.* FROM ${TABLES.COMMENTS} p JOIN comments_needing_update cnu ON p.cid = cnu.parentCid
|
|
1672
|
-
WHERE p.cid IS NOT NULL AND (p.pendingApproval IS NULL OR p.pendingApproval != 1)
|
|
1641
|
+
GROUP BY c.parentCid
|
|
1642
|
+
)
|
|
1643
|
+
SELECT parent.rowid
|
|
1644
|
+
FROM ${TABLES.COMMENT_UPDATES} cu_parent
|
|
1645
|
+
JOIN ${TABLES.COMMENTS} parent ON parent.cid = cu_parent.cid
|
|
1646
|
+
LEFT JOIN last_child_rowids lc ON lc.cid = cu_parent.cid
|
|
1647
|
+
LEFT JOIN ${TABLES.COMMENTS} last_child ON last_child.rowid = lc.last_child_rowid
|
|
1648
|
+
WHERE ${notPending("parent")}
|
|
1649
|
+
AND COALESCE(last_child.cid, '') != COALESCE(cu_parent.lastChildCid, '')`,
|
|
1650
|
+
`${insert}
|
|
1651
|
+
SELECT DISTINCT parent.rowid
|
|
1652
|
+
FROM ${TABLES.COMMENT_UPDATES} cu_parent
|
|
1653
|
+
JOIN ${TABLES.COMMENTS} parent ON parent.cid = cu_parent.cid
|
|
1654
|
+
CROSS JOIN json_each(cu_parent.replies) sort_entry
|
|
1655
|
+
CROSS JOIN json_each(json_extract(sort_entry.value, '$.commentCids')) child_ref
|
|
1656
|
+
JOIN ${TABLES.COMMENT_UPDATES} cu_child ON cu_child.cid = child_ref.value
|
|
1657
|
+
WHERE cu_parent.replies IS NOT NULL
|
|
1658
|
+
AND json_type(sort_entry.value, '$.commentCids') = 'array'
|
|
1659
|
+
-- Compare against the parent's updatedAt, not its insertedAt (issue #230). insertedAt is
|
|
1660
|
+
-- the batch's start timestamp, shared by every row the batch writes (issue #209/#211),
|
|
1661
|
+
-- while updatedAt is stamped when that row is actually calculated. A batch walks a post
|
|
1662
|
+
-- tree deepest-depth-first, so a child is always calculated before its parent and its
|
|
1663
|
+
-- updatedAt lands after the batch's start: comparing against insertedAt re-flagged every
|
|
1664
|
+
-- parent of any batch spanning >= 1 second, which produced another such batch, and the
|
|
1665
|
+
-- update loop never converged. updatedAt is when the parent's replies page was actually
|
|
1666
|
+
-- generated, which is the point the child's data could have gone stale relative to.
|
|
1667
|
+
AND cu_child.updatedAt > cu_parent.updatedAt`,
|
|
1668
|
+
...windowed.selects.map((select) => `${insert} WITH RECURSIVE ${windowed.ctes} ${select}`),
|
|
1669
|
+
`${insert}
|
|
1670
|
+
SELECT c.rowid FROM ${TABLES.COMMENTS} c
|
|
1671
|
+
WHERE ${notPending("c")}
|
|
1672
|
+
AND c.authorSignerAddress IN (
|
|
1673
|
+
SELECT flagged.authorSignerAddress FROM ${COMMENTS_TO_UPDATE_TEMP_TABLE} t
|
|
1674
|
+
JOIN ${TABLES.COMMENTS} flagged ON flagged.rowid = t.id
|
|
1675
|
+
)`,
|
|
1676
|
+
`${insert}
|
|
1677
|
+
WITH RECURSIVE parent_chain(id, parentCid) AS (
|
|
1678
|
+
SELECT p.rowid, p.parentCid FROM ${COMMENTS_TO_UPDATE_TEMP_TABLE} t
|
|
1679
|
+
JOIN ${TABLES.COMMENTS} c ON c.rowid = t.id
|
|
1680
|
+
JOIN ${TABLES.COMMENTS} p ON p.cid = c.parentCid
|
|
1681
|
+
WHERE ${notPending("p")}
|
|
1673
1682
|
UNION
|
|
1674
|
-
SELECT
|
|
1675
|
-
WHERE
|
|
1676
|
-
),
|
|
1677
|
-
all_updates AS (
|
|
1678
|
-
SELECT cid FROM comments_needing_update UNION SELECT cid FROM parent_chain
|
|
1683
|
+
SELECT p.rowid, p.parentCid FROM parent_chain pc JOIN ${TABLES.COMMENTS} p ON p.cid = pc.parentCid
|
|
1684
|
+
WHERE ${notPending("p")}
|
|
1679
1685
|
)
|
|
1680
|
-
SELECT
|
|
1686
|
+
SELECT id FROM parent_chain`
|
|
1687
|
+
];
|
|
1688
|
+
for (const arm of arms) {
|
|
1689
|
+
// Each statement is bound only to the named parameters it mentions (the windowed arms' window bounds)
|
|
1690
|
+
const params = Object.fromEntries(Object.entries(windowed.params).filter(([name]) => arm.includes(`:${name}`)));
|
|
1691
|
+
this._prepareCached(arm).run(params);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
// Runs `read` with the temp table of comments to update filled, and empties it afterwards whatever happens.
|
|
1695
|
+
// The table is one per connection: the update loop is the only caller on a community's own connection, and
|
|
1696
|
+
// a test handler is a connection of its own.
|
|
1697
|
+
_withCommentsToBeUpdated(read) {
|
|
1698
|
+
this._db.exec(`CREATE TEMP TABLE IF NOT EXISTS ${COMMENTS_TO_UPDATE_TEMP_TABLE} (id INTEGER PRIMARY KEY)`);
|
|
1699
|
+
this._db.exec(`DELETE FROM ${COMMENTS_TO_UPDATE_TEMP_TABLE}`);
|
|
1700
|
+
try {
|
|
1701
|
+
this._collectCommentsToBeUpdated();
|
|
1702
|
+
return read();
|
|
1703
|
+
}
|
|
1704
|
+
finally {
|
|
1705
|
+
this._db.exec(`DELETE FROM ${COMMENTS_TO_UPDATE_TEMP_TABLE}`);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
// Cached, so every caller sets raw() itself: the mode sticks to the statement object
|
|
1709
|
+
_selectCommentsToBeUpdated() {
|
|
1710
|
+
return this._prepareCached(`
|
|
1711
|
+
SELECT c.cid, c.depth FROM ${COMMENTS_TO_UPDATE_TEMP_TABLE} t JOIN ${TABLES.COMMENTS} c ON c.rowid = t.id
|
|
1681
1712
|
WHERE (c.pendingApproval IS NULL OR c.pendingApproval != 1)
|
|
1682
|
-
ORDER BY
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1713
|
+
ORDER BY t.id
|
|
1714
|
+
`);
|
|
1715
|
+
}
|
|
1716
|
+
// Which comments the update cycle has to recalculate, as cid and depth only, in rowid order. Each slice of the
|
|
1717
|
+
// cycle loads its own rows through queryCommentsByCids. See _collectCommentsToBeUpdated for what flags a comment.
|
|
1718
|
+
queryCommentsToBeUpdated() {
|
|
1719
|
+
return this._withCommentsToBeUpdated(() => this._selectCommentsToBeUpdated().raw(false).all());
|
|
1720
|
+
}
|
|
1721
|
+
// The same set as queryCommentsToBeUpdated, grouped by depth as plain arrays of cids in rowid order: what the update
|
|
1722
|
+
// cycle holds for the whole board is then one string per comment to update (3.1M comments: 250 MB against 750 MB
|
|
1723
|
+
// of {cid, depth} objects, and 2.7 GB of full rows before issue #355).
|
|
1724
|
+
queryCidsToBeUpdatedByDepth() {
|
|
1725
|
+
return this._withCommentsToBeUpdated(() => {
|
|
1726
|
+
const cidsByDepth = new Map();
|
|
1727
|
+
for (const [cid, depth] of this._selectCommentsToBeUpdated().raw(true).iterate()) {
|
|
1728
|
+
const cids = cidsByDepth.get(depth);
|
|
1729
|
+
if (cids)
|
|
1730
|
+
cids.push(cid);
|
|
1731
|
+
else
|
|
1732
|
+
cidsByDepth.set(depth, [cid]);
|
|
1733
|
+
}
|
|
1734
|
+
return cidsByDepth;
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
// The comment rows behind a list of cids, parsed, in the order the cids were given (a cid with no row is
|
|
1738
|
+
// skipped). What each slice of the update cycle loads for itself, since queryCommentsToBeUpdated no longer
|
|
1739
|
+
// materializes the rows for the whole board (issue #355).
|
|
1740
|
+
queryCommentsByCids(cids) {
|
|
1741
|
+
const byCid = new Map();
|
|
1742
|
+
this._forEachCidBatch(cids, (placeholders) => `SELECT * FROM ${TABLES.COMMENTS} WHERE cid IN (${placeholders})`, (row) => byCid.set(row.cid, this._parseCommentsTableRow(row)));
|
|
1743
|
+
const rows = [];
|
|
1744
|
+
for (const cid of cids) {
|
|
1745
|
+
const row = byCid.get(cid);
|
|
1746
|
+
if (row)
|
|
1747
|
+
rows.push(row);
|
|
1748
|
+
}
|
|
1749
|
+
return rows;
|
|
1686
1750
|
}
|
|
1687
1751
|
queryCommunityStats() {
|
|
1688
1752
|
// if you change this logic, make sure to run stats.community.test.js
|
|
@@ -1868,18 +1932,16 @@ export class DbHandler {
|
|
|
1868
1932
|
return hash;
|
|
1869
1933
|
}
|
|
1870
1934
|
queryComment(cid) {
|
|
1871
|
-
const row = this.
|
|
1935
|
+
const row = this._prepareCached(`SELECT * FROM ${TABLES.COMMENTS} WHERE cid = ?`).get(cid);
|
|
1872
1936
|
if (!row)
|
|
1873
1937
|
return undefined;
|
|
1874
1938
|
return this._parseCommentsTableRow(row);
|
|
1875
1939
|
}
|
|
1876
1940
|
commentExistsInDb(cid) {
|
|
1877
|
-
return this.
|
|
1941
|
+
return this._prepareCached(`SELECT 1 FROM ${TABLES.COMMENTS} WHERE cid = ? LIMIT 1`).get(cid) !== undefined;
|
|
1878
1942
|
}
|
|
1879
1943
|
queryPseudonymityAliasByCommentCid(commentCid) {
|
|
1880
|
-
const row = this.
|
|
1881
|
-
.prepare(`SELECT commentCid, aliasPrivateKey, originalAuthorPublicKey, originalAuthorName, mode, insertedAt FROM ${TABLES.PSEUDONYMITY_ALIASES} WHERE commentCid = ?`)
|
|
1882
|
-
.get(commentCid);
|
|
1944
|
+
const row = this._prepareCached(`SELECT commentCid, aliasPrivateKey, originalAuthorPublicKey, originalAuthorName, mode, insertedAt FROM ${TABLES.PSEUDONYMITY_ALIASES} WHERE commentCid = ?`).get(commentCid);
|
|
1883
1945
|
return row;
|
|
1884
1946
|
}
|
|
1885
1947
|
queryPseudonymityAliasForPost(originalAuthorPublicKey, postCid) {
|
|
@@ -1915,35 +1977,6 @@ export class DbHandler {
|
|
|
1915
1977
|
const parentCid = typeof row.parentCid === "string" ? row.parentCid : row.parentCid === null ? null : undefined;
|
|
1916
1978
|
return { authorSignerAddress, parentCid };
|
|
1917
1979
|
}
|
|
1918
|
-
_queryCommentCounts(cid) {
|
|
1919
|
-
const { clause: addrClause, params: addrParams } = this._communityAddressClauseNamed("c", "cc");
|
|
1920
|
-
const query = `
|
|
1921
|
-
SELECT
|
|
1922
|
-
(SELECT COUNT(*) FROM ${TABLES.VOTES} WHERE commentCid = :cid AND vote = 1) AS upvoteCount,
|
|
1923
|
-
(SELECT COUNT(*) FROM ${TABLES.VOTES} WHERE commentCid = :cid AND vote = -1) AS downvoteCount,
|
|
1924
|
-
(
|
|
1925
|
-
WITH RECURSIVE descendants AS (
|
|
1926
|
-
SELECT c.cid FROM ${TABLES.COMMENTS} c
|
|
1927
|
-
INNER JOIN ${TABLES.COMMENT_UPDATES} cu ON c.cid = cu.cid
|
|
1928
|
-
LEFT JOIN (SELECT cid, json_extract(edit, '$.deleted') AS deleted_flag FROM ${TABLES.COMMENT_UPDATES}) AS d ON c.cid = d.cid
|
|
1929
|
-
WHERE c.parentCid = :cid AND ${addrClause} AND (cu.removed IS NOT 1 AND cu.removed IS NOT TRUE) AND (d.deleted_flag IS NULL OR d.deleted_flag != 1)
|
|
1930
|
-
UNION ALL
|
|
1931
|
-
SELECT c.cid FROM ${TABLES.COMMENTS} c
|
|
1932
|
-
INNER JOIN ${TABLES.COMMENT_UPDATES} cu ON c.cid = cu.cid
|
|
1933
|
-
LEFT JOIN (SELECT cid, json_extract(edit, '$.deleted') AS deleted_flag FROM ${TABLES.COMMENT_UPDATES}) AS d ON c.cid = d.cid
|
|
1934
|
-
JOIN descendants desc_nodes ON c.parentCid = desc_nodes.cid
|
|
1935
|
-
WHERE ${addrClause} AND (cu.removed IS NOT 1 AND cu.removed IS NOT TRUE) AND (d.deleted_flag IS NULL OR d.deleted_flag != 1)
|
|
1936
|
-
) SELECT COUNT(*) FROM descendants
|
|
1937
|
-
) AS replyCount,
|
|
1938
|
-
(
|
|
1939
|
-
SELECT COUNT(*) FROM ${TABLES.COMMENTS} c
|
|
1940
|
-
INNER JOIN ${TABLES.COMMENT_UPDATES} cu ON c.cid = cu.cid
|
|
1941
|
-
LEFT JOIN (SELECT cid, json_extract(edit, '$.deleted') AS deleted_flag FROM ${TABLES.COMMENT_UPDATES}) AS d ON c.cid = d.cid
|
|
1942
|
-
WHERE c.parentCid = :cid AND ${addrClause} AND (cu.removed IS NOT 1 AND cu.removed IS NOT TRUE) AND (d.deleted_flag IS NULL OR d.deleted_flag != 1)
|
|
1943
|
-
) AS childCount
|
|
1944
|
-
`;
|
|
1945
|
-
return this._db.prepare(query).get({ cid, ...addrParams });
|
|
1946
|
-
}
|
|
1947
1980
|
queryPostsWithOutdatedBuckets(buckets) {
|
|
1948
1981
|
const currentTimestampSeconds = timestamp(); // timestamp is in seconds
|
|
1949
1982
|
const maxBucket = Math.max(...buckets);
|
|
@@ -1963,7 +1996,7 @@ export class DbHandler {
|
|
|
1963
1996
|
) SELECT cid, timestamp, current_bucket AS currentBucket, new_bucket AS newBucket
|
|
1964
1997
|
FROM post_data WHERE current_bucket != new_bucket
|
|
1965
1998
|
`;
|
|
1966
|
-
return this.
|
|
1999
|
+
return this._prepareCached(query).all(...addrParams, maxBucket);
|
|
1967
2000
|
}
|
|
1968
2001
|
queryLatestAuthorEdit(cid, authorSignerAddress) {
|
|
1969
2002
|
const row = this._db
|
|
@@ -1981,6 +2014,7 @@ export class DbHandler {
|
|
|
1981
2014
|
return pick(parsed, ["signature", ...signedKeys, ...commentEditFields]);
|
|
1982
2015
|
}
|
|
1983
2016
|
removeCommentFromPendingApproval(comment) {
|
|
2017
|
+
this._publicationWriteGeneration++;
|
|
1984
2018
|
const log = Logger("pkc-js:local-community:db-handler:removeCommentFromPendingApproval");
|
|
1985
2019
|
const stmt = this._db.prepare(`UPDATE ${TABLES.COMMENTS} SET pendingApproval = 0 WHERE cid = ?`);
|
|
1986
2020
|
const res = stmt.run(comment.cid);
|
|
@@ -2112,17 +2146,6 @@ export class DbHandler {
|
|
|
2112
2146
|
}
|
|
2113
2147
|
return purgedDetails;
|
|
2114
2148
|
}
|
|
2115
|
-
_queryLatestModeratorReason(comment) {
|
|
2116
|
-
const result = this._db
|
|
2117
|
-
.prepare(`
|
|
2118
|
-
SELECT json_extract(commentModeration, '$.reason') AS reason FROM ${TABLES.COMMENT_MODERATIONS}
|
|
2119
|
-
WHERE commentCid = ? AND json_extract(commentModeration, '$.reason') IS NOT NULL ORDER BY rowid DESC LIMIT 1
|
|
2120
|
-
`)
|
|
2121
|
-
.get(comment.cid);
|
|
2122
|
-
if (!result)
|
|
2123
|
-
return undefined;
|
|
2124
|
-
return result;
|
|
2125
|
-
}
|
|
2126
2149
|
queryCommentFlagsSetByMod(cid) {
|
|
2127
2150
|
const query = `
|
|
2128
2151
|
WITH flags_with_rank AS (
|
|
@@ -2148,7 +2171,7 @@ export class DbHandler {
|
|
|
2148
2171
|
MAX(CASE WHEN nsfw IS NOT NULL AND nsfw_rank = 1 THEN nsfw ELSE NULL END) AS nsfw
|
|
2149
2172
|
FROM flags_with_rank
|
|
2150
2173
|
`;
|
|
2151
|
-
const flags = this.
|
|
2174
|
+
const flags = this._prepareCached(query).get(cid);
|
|
2152
2175
|
if (!flags)
|
|
2153
2176
|
return {};
|
|
2154
2177
|
return mapValues(removeNullUndefinedValues(flags), Boolean);
|
|
@@ -2162,141 +2185,507 @@ export class DbHandler {
|
|
|
2162
2185
|
.get(cid);
|
|
2163
2186
|
return result && result.deleted !== null ? { deleted: Boolean(result.deleted) } : undefined;
|
|
2164
2187
|
}
|
|
2165
|
-
|
|
2166
|
-
const result = this.
|
|
2167
|
-
.
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
.get(comment.cid);
|
|
2172
|
-
if (!result)
|
|
2188
|
+
_queryIsCommentApproved(comment) {
|
|
2189
|
+
const result = this._prepareCached(`
|
|
2190
|
+
SELECT json_extract(commentModeration, '$.approved') AS approved FROM ${TABLES.COMMENT_MODERATIONS}
|
|
2191
|
+
WHERE commentCid = ? AND json_extract(commentModeration, '$.approved') IS NOT NULL ORDER BY rowid DESC LIMIT 1
|
|
2192
|
+
`).get(comment.cid);
|
|
2193
|
+
if (!result || result.approved === null)
|
|
2173
2194
|
return undefined;
|
|
2174
|
-
return {
|
|
2195
|
+
return { approved: Boolean(result.approved) };
|
|
2175
2196
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2197
|
+
queryCalculatedCommentUpdate(opts) {
|
|
2198
|
+
// The batch of one: the per-comment API is the batched calculation over a single entry (issue #352)
|
|
2199
|
+
return this._calculateCommentUpdates([{ comment: opts.comment, authorDomain: opts.authorDomain }], createCommunityAuthorMemo()).get(opts.comment.cid);
|
|
2200
|
+
}
|
|
2201
|
+
// The CommentUpdate fields of every given comment in one pass (issue #352): each field group is one statement per
|
|
2202
|
+
// chunk of comments instead of nine statements per comment, and the author aggregates are computed once per
|
|
2203
|
+
// distinct author (address set + domain) and kept in `authorMemo`, which the update cycle shares across its
|
|
2204
|
+
// depth batches so an author with a thousand comments is aggregated once per cycle, or once per publication
|
|
2205
|
+
// write (a memo filled before a vote landed would pair that vote's fresh counts with a stale score). Same result as
|
|
2206
|
+
// queryCalculatedCommentUpdate for every comment (the per-comment API is this over one entry).
|
|
2207
|
+
queryCalculatedCommentUpdates(opts) {
|
|
2208
|
+
return this._calculateCommentUpdates(opts.comments.map((comment) => ({ comment, authorDomain: getAuthorNameFromWire(comment.author) })), opts.authorMemo ?? createCommunityAuthorMemo());
|
|
2209
|
+
}
|
|
2210
|
+
_calculateCommentUpdates(entries, authorMemo) {
|
|
2211
|
+
const result = new Map();
|
|
2212
|
+
if (entries.length === 0)
|
|
2213
|
+
return result;
|
|
2214
|
+
const cids = entries.map((entry) => entry.comment.cid);
|
|
2215
|
+
const authorByCid = this._queryCommunityAuthorsForCommentUpdates(entries, authorMemo);
|
|
2216
|
+
const votes = this._queryVoteCountsByCids(cids);
|
|
2217
|
+
const counts = this._queryReplyCountsByParentCids(cids);
|
|
2218
|
+
const lastReplyTimestamps = this._queryLastReplyTimestampsByParentCids(cids);
|
|
2219
|
+
const lastChildCids = this._queryLastChildCidsByParentCids(cids);
|
|
2220
|
+
const moderations = this._queryModerationSummariesByCids(cids);
|
|
2221
|
+
const authorEdits = this._queryLatestAuthorEditsByCids(entries.map((entry) => entry.comment));
|
|
2222
|
+
const numbers = this._queryCommentNumbersByCids(cids);
|
|
2223
|
+
for (const { comment } of entries) {
|
|
2224
|
+
const authorCommunity = authorByCid.get(comment.cid);
|
|
2225
|
+
if (!authorCommunity)
|
|
2226
|
+
throw Error("Failed to query author.community in queryCalculatedCommentUpdate");
|
|
2227
|
+
const moderation = moderations.get(comment.cid);
|
|
2228
|
+
const authorEdit = authorEdits.get(comment.cid);
|
|
2229
|
+
const isThisCommentApproved = moderation?.approved !== undefined ? { approved: moderation.approved } : undefined;
|
|
2230
|
+
const removedFromApproved = isThisCommentApproved?.approved === false ? { removed: true } : undefined; // automatically add removed:true if approved=false. Will be overridden if there's commentFlags.removed
|
|
2231
|
+
const { number: commentNumber, postNumber } = numbers.get(comment.cid) ?? {};
|
|
2232
|
+
const voteCounts = votes.get(comment.cid) ?? { upvoteCount: 0, downvoteCount: 0 };
|
|
2233
|
+
const replyCounts = counts.get(comment.cid) ?? { replyCount: 0, childCount: 0 };
|
|
2234
|
+
// Seed with challenge-supplied commentUpdate (lowest priority, per-field). Mod queries below
|
|
2235
|
+
// overwrite individual keys (reason, flairs, flags, approved, ...) when the mod has actually
|
|
2236
|
+
// published a moderation that set that key — challenge keys the mod never touched persist.
|
|
2237
|
+
// Same logic applies one level deeper for author.community: challenge-supplied
|
|
2238
|
+
// commentUpdate.author.community.<newKey> (e.g. countryCode) seeds underneath the computed
|
|
2239
|
+
// authorCommunity, so community-computed keys (postScore, replyScore, ...) and mod-settable
|
|
2240
|
+
// keys (flairs, banExpiresAt) always win. The validator forbids challenges from setting any
|
|
2241
|
+
// schema-defined key on author.community, so the spread here only carries novel extras.
|
|
2242
|
+
const challengeAuthorCommunity = comment.challengeCommentUpdate?.author
|
|
2243
|
+
?.community;
|
|
2244
|
+
result.set(comment.cid, {
|
|
2245
|
+
...(comment.challengeCommentUpdate ?? {}),
|
|
2246
|
+
...(removedFromApproved ? removedFromApproved : undefined),
|
|
2247
|
+
cid: comment.cid,
|
|
2248
|
+
...(commentNumber !== undefined ? { number: commentNumber } : undefined),
|
|
2249
|
+
...(postNumber !== undefined ? { postNumber } : undefined),
|
|
2250
|
+
upvoteCount: voteCounts.upvoteCount,
|
|
2251
|
+
downvoteCount: voteCounts.downvoteCount,
|
|
2252
|
+
replyCount: replyCounts.replyCount,
|
|
2253
|
+
childCount: replyCounts.childCount,
|
|
2254
|
+
flairs: moderation?.flairs || authorEdit?.flairs || comment.challengeCommentUpdate?.flairs,
|
|
2255
|
+
...moderation?.flags,
|
|
2256
|
+
// moderatorReason wins when present, else fall back to the challenge-supplied reason (if any).
|
|
2257
|
+
reason: moderation?.reason ?? comment.challengeCommentUpdate?.reason,
|
|
2258
|
+
author: { community: { ...(challengeAuthorCommunity ?? {}), ...authorCommunity } },
|
|
2259
|
+
lastChildCid: lastChildCids.get(comment.cid),
|
|
2260
|
+
lastReplyTimestamp: lastReplyTimestamps.get(comment.cid),
|
|
2261
|
+
...(authorEdit ? { edit: authorEdit } : undefined),
|
|
2262
|
+
...(isThisCommentApproved ? { approved: isThisCommentApproved.approved } : undefined)
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
return result;
|
|
2266
|
+
}
|
|
2267
|
+
// Chunks of a cid list under the SQLite variable cap, each padded with NULLs to a power of two so a query has at
|
|
2268
|
+
// most 13 prepared variants instead of one per batch size (`x IN (..., NULL)` never matches the padding).
|
|
2269
|
+
_cidChunks(cids, variablesBesidesCids = 0) {
|
|
2270
|
+
const max = 4096 - variablesBesidesCids;
|
|
2271
|
+
const chunks = [];
|
|
2272
|
+
for (let start = 0; start < cids.length; start += max) {
|
|
2273
|
+
const chunk = cids.slice(start, start + max);
|
|
2274
|
+
let size = 1;
|
|
2275
|
+
while (size < chunk.length)
|
|
2276
|
+
size *= 2;
|
|
2277
|
+
size = Math.min(size, max);
|
|
2278
|
+
chunks.push({
|
|
2279
|
+
placeholders: new Array(size).fill("?").join(","),
|
|
2280
|
+
params: [...chunk, ...new Array(size - chunk.length).fill(null)]
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
return chunks;
|
|
2284
|
+
}
|
|
2285
|
+
_queryVoteCountsByCids(cids) {
|
|
2286
|
+
const result = new Map();
|
|
2287
|
+
for (const { placeholders, params } of this._cidChunks(cids)) {
|
|
2288
|
+
const rows = this._prepareCached(`SELECT commentCid,
|
|
2289
|
+
SUM(CASE WHEN vote = 1 THEN 1 ELSE 0 END) AS upvoteCount,
|
|
2290
|
+
SUM(CASE WHEN vote = -1 THEN 1 ELSE 0 END) AS downvoteCount
|
|
2291
|
+
FROM ${TABLES.VOTES} WHERE commentCid IN (${placeholders}) GROUP BY commentCid`).all(...params);
|
|
2292
|
+
for (const row of rows)
|
|
2293
|
+
result.set(row.commentCid, { upvoteCount: row.upvoteCount, downvoteCount: row.downvoteCount });
|
|
2294
|
+
}
|
|
2295
|
+
return result;
|
|
2296
|
+
}
|
|
2297
|
+
// replyCount and childCount of every parent in one recursive walk anchored on all of them: a descendant counts
|
|
2298
|
+
// when every comment on the path from the parent has a CommentUpdate, this community's address, and is neither
|
|
2299
|
+
// removed nor deleted; childCount is the level-1 part of the same walk.
|
|
2300
|
+
_queryReplyCountsByParentCids(parentCids) {
|
|
2301
|
+
const result = new Map();
|
|
2302
|
+
const { clause: addrClause, params: addrParams } = this._communityAddressClause("c");
|
|
2303
|
+
const passes = `${addrClause} AND (cu.removed IS NOT 1 AND cu.removed IS NOT TRUE) AND (d.deleted_flag IS NULL OR d.deleted_flag != 1)`;
|
|
2304
|
+
const deletedLookup = `(SELECT cid, json_extract(edit, '$.deleted') AS deleted_flag FROM ${TABLES.COMMENT_UPDATES})`;
|
|
2305
|
+
for (const { placeholders, params } of this._cidChunks(parentCids, addrParams.length * 2)) {
|
|
2306
|
+
const rows = this._prepareCached(`WITH RECURSIVE descendants(root, cid, level) AS (
|
|
2307
|
+
SELECT c.parentCid, c.cid, 1 FROM ${TABLES.COMMENTS} c
|
|
2308
|
+
INNER JOIN ${TABLES.COMMENT_UPDATES} cu ON c.cid = cu.cid
|
|
2309
|
+
LEFT JOIN ${deletedLookup} AS d ON c.cid = d.cid
|
|
2310
|
+
WHERE c.parentCid IN (${placeholders}) AND ${passes}
|
|
2311
|
+
UNION ALL
|
|
2312
|
+
SELECT desc_nodes.root, c.cid, desc_nodes.level + 1 FROM ${TABLES.COMMENTS} c
|
|
2313
|
+
INNER JOIN ${TABLES.COMMENT_UPDATES} cu ON c.cid = cu.cid
|
|
2314
|
+
LEFT JOIN ${deletedLookup} AS d ON c.cid = d.cid
|
|
2315
|
+
JOIN descendants desc_nodes ON c.parentCid = desc_nodes.cid
|
|
2316
|
+
WHERE ${passes}
|
|
2317
|
+
)
|
|
2318
|
+
SELECT root, COUNT(*) AS replyCount, SUM(CASE WHEN level = 1 THEN 1 ELSE 0 END) AS childCount
|
|
2319
|
+
FROM descendants GROUP BY root`).all(...params, ...addrParams, ...addrParams);
|
|
2320
|
+
for (const row of rows)
|
|
2321
|
+
result.set(row.root, { replyCount: row.replyCount, childCount: row.childCount });
|
|
2322
|
+
}
|
|
2323
|
+
return result;
|
|
2324
|
+
}
|
|
2325
|
+
// lastReplyTimestamp of every parent: the newest timestamp under it. A direct reply counts unless it is pending or
|
|
2326
|
+
// disapproved (a reply with no CommentUpdate yet counts); a deeper one also needs a CommentUpdate, this
|
|
2327
|
+
// community's address and to be neither removed nor deleted, and so does every comment on its path.
|
|
2328
|
+
_queryLastReplyTimestampsByParentCids(parentCids) {
|
|
2329
|
+
const result = new Map();
|
|
2330
|
+
const { clause: addrClause, params: addrParams } = this._communityAddressClause("c");
|
|
2331
|
+
for (const { placeholders, params } of this._cidChunks(parentCids, addrParams.length)) {
|
|
2332
|
+
const rows = this._prepareCached(`WITH RECURSIVE descendants(root, cid, timestamp) AS (
|
|
2333
|
+
SELECT c.parentCid, c.cid, c.timestamp FROM ${TABLES.COMMENTS} c
|
|
2334
|
+
LEFT JOIN ${TABLES.COMMENT_UPDATES} cu ON cu.cid = c.cid
|
|
2335
|
+
WHERE c.parentCid IN (${placeholders})
|
|
2336
|
+
AND COALESCE(cu.approved, 1) != 0
|
|
2337
|
+
AND (c.pendingApproval IS NULL OR c.pendingApproval != 1)
|
|
2338
|
+
UNION ALL
|
|
2339
|
+
SELECT desc_nodes.root, c.cid, c.timestamp FROM ${TABLES.COMMENTS} c
|
|
2340
|
+
INNER JOIN ${TABLES.COMMENT_UPDATES} cu ON c.cid = cu.cid
|
|
2341
|
+
LEFT JOIN (SELECT cid, json_extract(edit, '$.deleted') AS deleted_flag FROM ${TABLES.COMMENT_UPDATES}) AS d ON c.cid = d.cid
|
|
2342
|
+
JOIN descendants desc_nodes ON c.parentCid = desc_nodes.cid
|
|
2343
|
+
WHERE ${addrClause} AND (cu.removed IS NOT 1 AND cu.removed IS NOT TRUE) AND (d.deleted_flag IS NULL OR d.deleted_flag != 1)
|
|
2344
|
+
AND COALESCE(cu.approved, 1) != 0
|
|
2345
|
+
AND (c.pendingApproval IS NULL OR c.pendingApproval != 1)
|
|
2346
|
+
)
|
|
2347
|
+
SELECT root, MAX(timestamp) AS maxTimestamp FROM descendants GROUP BY root`).all(...params, ...addrParams);
|
|
2348
|
+
for (const row of rows)
|
|
2349
|
+
if (row.maxTimestamp !== null)
|
|
2350
|
+
result.set(row.root, row.maxTimestamp);
|
|
2351
|
+
}
|
|
2352
|
+
return result;
|
|
2353
|
+
}
|
|
2354
|
+
// lastChildCid of every parent: its newest direct reply that has a CommentUpdate and is not pending, disapproved,
|
|
2355
|
+
// removed or deleted (MAX(rowid) with the bare cid column: SQLite returns the cid of that row).
|
|
2356
|
+
_queryLastChildCidsByParentCids(parentCids) {
|
|
2357
|
+
const result = new Map();
|
|
2358
|
+
for (const { placeholders, params } of this._cidChunks(parentCids)) {
|
|
2359
|
+
const rows = this._prepareCached(`SELECT c.parentCid AS parentCid, c.cid AS cid, MAX(c.rowid) AS lastRowid FROM ${TABLES.COMMENTS} c
|
|
2179
2360
|
INNER JOIN ${TABLES.COMMENT_UPDATES} cu ON cu.cid = c.cid
|
|
2180
2361
|
LEFT JOIN (
|
|
2181
2362
|
SELECT cid, json_extract(edit, '$.deleted') AS deleted_flag FROM ${TABLES.COMMENT_UPDATES}
|
|
2182
2363
|
) deleted_lookup ON deleted_lookup.cid = c.cid
|
|
2183
|
-
WHERE c.parentCid
|
|
2364
|
+
WHERE c.parentCid IN (${placeholders})
|
|
2184
2365
|
AND (c.pendingApproval IS NULL OR c.pendingApproval != 1)
|
|
2185
2366
|
AND COALESCE(cu.approved, 1) != 0
|
|
2186
2367
|
AND (cu.removed IS NOT 1 AND cu.removed IS NOT TRUE)
|
|
2187
2368
|
AND (deleted_lookup.deleted_flag IS NULL OR deleted_lookup.deleted_flag != 1)
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
return
|
|
2369
|
+
GROUP BY c.parentCid`).all(...params);
|
|
2370
|
+
for (const row of rows)
|
|
2371
|
+
result.set(row.parentCid, row.cid);
|
|
2372
|
+
}
|
|
2373
|
+
return result;
|
|
2193
2374
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
.
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2375
|
+
// What the moderations of each comment say, newest first, one read for the batch: the latest non-null value per
|
|
2376
|
+
// field (reason, flairs, approved and the six flags), the same precedence as the per-field statements this replaces.
|
|
2377
|
+
_queryModerationSummariesByCids(cids) {
|
|
2378
|
+
const FLAG_NAMES = ["spoiler", "pinned", "locked", "archived", "removed", "nsfw"];
|
|
2379
|
+
const result = new Map();
|
|
2380
|
+
for (const { placeholders, params } of this._cidChunks(cids)) {
|
|
2381
|
+
const rows = this._prepareCached(`SELECT commentCid, commentModeration FROM ${TABLES.COMMENT_MODERATIONS} WHERE commentCid IN (${placeholders}) ORDER BY rowid DESC`).all(...params);
|
|
2382
|
+
for (const row of rows) {
|
|
2383
|
+
const moderation = JSON.parse(row.commentModeration);
|
|
2384
|
+
let summary = result.get(row.commentCid);
|
|
2385
|
+
if (!summary) {
|
|
2386
|
+
summary = { flags: {} };
|
|
2387
|
+
result.set(row.commentCid, summary);
|
|
2388
|
+
}
|
|
2389
|
+
if (summary.reason === undefined && typeof moderation.reason === "string")
|
|
2390
|
+
summary.reason = moderation.reason;
|
|
2391
|
+
if (summary.flairs === undefined && moderation.flairs !== null && moderation.flairs !== undefined)
|
|
2392
|
+
summary.flairs = moderation.flairs;
|
|
2393
|
+
if (summary.approved === undefined && moderation.approved !== null && moderation.approved !== undefined)
|
|
2394
|
+
summary.approved = Boolean(moderation.approved);
|
|
2395
|
+
for (const flag of FLAG_NAMES)
|
|
2396
|
+
if (summary.flags[flag] === undefined && moderation[flag] !== null && moderation[flag] !== undefined)
|
|
2397
|
+
summary.flags[flag] = Boolean(moderation[flag]);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
return result;
|
|
2204
2401
|
}
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
.
|
|
2218
|
-
.get(cid);
|
|
2219
|
-
if (commentNumber === undefined && typeof existingNumbers?.number === "number" && existingNumbers.number > 0)
|
|
2220
|
-
commentNumber = existingNumbers.number;
|
|
2221
|
-
if (commentRowMeta.depth === 0 && postNumber === undefined) {
|
|
2222
|
-
if (typeof existingNumbers?.postNumber === "number" && existingNumbers.postNumber > 0)
|
|
2223
|
-
postNumber = existingNumbers.postNumber;
|
|
2402
|
+
// The latest edit each comment's own author published, one read for the batch
|
|
2403
|
+
_queryLatestAuthorEditsByCids(comments) {
|
|
2404
|
+
const result = new Map();
|
|
2405
|
+
const authorOf = new Map(comments.map((comment) => [comment.cid, comment.authorSignerAddress]));
|
|
2406
|
+
const commentEditFields = keys(CommentEditPubsubMessagePublicationSchema.shape);
|
|
2407
|
+
for (const { placeholders, params } of this._cidChunks(comments.map((comment) => comment.cid))) {
|
|
2408
|
+
const rows = this._prepareCached(`SELECT * FROM ${TABLES.COMMENT_EDITS} WHERE commentCid IN (${placeholders}) AND isAuthorEdit = 1 ORDER BY rowid DESC`).all(...params);
|
|
2409
|
+
for (const row of rows) {
|
|
2410
|
+
if (result.has(row.commentCid) || authorOf.get(row.commentCid) !== row.authorSignerAddress)
|
|
2411
|
+
continue;
|
|
2412
|
+
const parsed = this._spreadExtraProps(this._parseCommentEditsRow(row));
|
|
2413
|
+
const signedKeys = parsed.signature.signedPropertyNames;
|
|
2414
|
+
result.set(row.commentCid, pick(parsed, ["signature", ...signedKeys, ...commentEditFields]));
|
|
2224
2415
|
}
|
|
2225
2416
|
}
|
|
2226
|
-
return
|
|
2227
|
-
...(commentNumber !== undefined ? { number: commentNumber } : undefined),
|
|
2228
|
-
...(postNumber !== undefined ? { postNumber } : undefined)
|
|
2229
|
-
};
|
|
2417
|
+
return result;
|
|
2230
2418
|
}
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
const
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
const
|
|
2240
|
-
const
|
|
2241
|
-
const
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2419
|
+
// number / postNumber of each comment: the comments row's, else the numbers an earlier CommentUpdate carried
|
|
2420
|
+
_queryCommentNumbersByCids(cids) {
|
|
2421
|
+
const metaByCid = new Map();
|
|
2422
|
+
for (const { placeholders, params } of this._cidChunks(cids)) {
|
|
2423
|
+
const rows = this._prepareCached(`SELECT cid, depth, pendingApproval, number, postNumber FROM ${TABLES.COMMENTS} WHERE cid IN (${placeholders})`).all(...params);
|
|
2424
|
+
for (const row of rows)
|
|
2425
|
+
metaByCid.set(row.cid, row);
|
|
2426
|
+
}
|
|
2427
|
+
const result = new Map();
|
|
2428
|
+
const needStored = [];
|
|
2429
|
+
for (const cid of cids) {
|
|
2430
|
+
const meta = metaByCid.get(cid);
|
|
2431
|
+
if (!meta)
|
|
2432
|
+
throw Error(`Failed to query row metadata for comment ${cid}`);
|
|
2433
|
+
if (meta.pendingApproval === 1) {
|
|
2434
|
+
result.set(cid, {});
|
|
2435
|
+
continue;
|
|
2436
|
+
}
|
|
2437
|
+
const number = typeof meta.number === "number" && meta.number > 0 ? meta.number : undefined;
|
|
2438
|
+
const postNumber = typeof meta.postNumber === "number" && meta.postNumber > 0 ? meta.postNumber : undefined;
|
|
2439
|
+
result.set(cid, {
|
|
2440
|
+
...(number !== undefined ? { number } : undefined),
|
|
2441
|
+
...(postNumber !== undefined ? { postNumber } : undefined)
|
|
2442
|
+
});
|
|
2443
|
+
if (number === undefined || (meta.depth === 0 && postNumber === undefined))
|
|
2444
|
+
needStored.push(cid);
|
|
2445
|
+
}
|
|
2446
|
+
for (const { placeholders, params } of this._cidChunks(needStored)) {
|
|
2447
|
+
const rows = this._prepareCached(`SELECT cid, number, postNumber FROM ${TABLES.COMMENT_UPDATES} WHERE cid IN (${placeholders})`).all(...params);
|
|
2448
|
+
for (const row of rows) {
|
|
2449
|
+
const meta = metaByCid.get(row.cid);
|
|
2450
|
+
const numbers = result.get(row.cid);
|
|
2451
|
+
if (numbers.number === undefined && typeof row.number === "number" && row.number > 0)
|
|
2452
|
+
numbers.number = row.number;
|
|
2453
|
+
if (meta.depth === 0 && numbers.postNumber === undefined && typeof row.postNumber === "number" && row.postNumber > 0)
|
|
2454
|
+
numbers.postNumber = row.postNumber;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
return result;
|
|
2458
|
+
}
|
|
2459
|
+
// author.community of every comment: the karma of the address set the comment's author is (an alias comment
|
|
2460
|
+
// counts the alias alone, a plain one every alias of the author too) plus the mod edits (bans, flairs) targeting
|
|
2461
|
+
// any of them or the domain. Aggregated once per distinct (karma set, mod-edit set, domain) and memoised for the
|
|
2462
|
+
// cycle, as long as nothing was written to the publication tables since the memo was filled; the aggregates
|
|
2463
|
+
// themselves are one GROUP BY per chunk of addresses.
|
|
2464
|
+
_queryCommunityAuthorsForCommentUpdates(entries, memo) {
|
|
2465
|
+
if (memo.writeGeneration !== this._publicationWriteGeneration) {
|
|
2466
|
+
memo.authors.clear();
|
|
2467
|
+
memo.writeGeneration = this._publicationWriteGeneration;
|
|
2468
|
+
}
|
|
2469
|
+
const aliasByCid = this._queryPseudonymityAliasesByCommentCids(entries.map((entry) => entry.comment.cid));
|
|
2470
|
+
const plainAddresses = [
|
|
2471
|
+
...new Set(entries.filter((entry) => !aliasByCid.has(entry.comment.cid)).map((entry) => entry.comment.authorSignerAddress))
|
|
2472
|
+
];
|
|
2473
|
+
const expanded = this._queryAliasExpandedAddressSets(plainAddresses);
|
|
2474
|
+
const keyOf = (karma, modEdit, domain) => `${[...karma].sort().join(",")}|${[...modEdit].sort().join(",")}|${domain ?? ""}`;
|
|
2475
|
+
const needByCid = new Map();
|
|
2476
|
+
for (const { comment, authorDomain } of entries) {
|
|
2477
|
+
const alias = aliasByCid.get(comment.cid);
|
|
2478
|
+
if (alias) {
|
|
2479
|
+
// Karma for just this alias, but mod edits (bans/flairs) from both the alias and the original author
|
|
2480
|
+
const modEdit = [comment.authorSignerAddress];
|
|
2481
|
+
try {
|
|
2482
|
+
const originalAddress = getPKCAddressFromPublicKeySync(alias.originalAuthorPublicKey);
|
|
2483
|
+
if (originalAddress !== comment.authorSignerAddress)
|
|
2484
|
+
modEdit.push(originalAddress);
|
|
2485
|
+
}
|
|
2486
|
+
catch {
|
|
2487
|
+
// ignore malformed keys
|
|
2488
|
+
}
|
|
2489
|
+
const domain = alias.originalAuthorName || authorDomain;
|
|
2490
|
+
needByCid.set(comment.cid, {
|
|
2491
|
+
karma: [comment.authorSignerAddress],
|
|
2492
|
+
modEdit,
|
|
2493
|
+
domain,
|
|
2494
|
+
key: keyOf([comment.authorSignerAddress], modEdit, domain)
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
else {
|
|
2498
|
+
const set = [...(expanded.get(comment.authorSignerAddress) ?? new Set([comment.authorSignerAddress]))];
|
|
2499
|
+
needByCid.set(comment.cid, { karma: set, modEdit: set, domain: authorDomain, key: keyOf(set, set, authorDomain) });
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
const uncached = new Map();
|
|
2503
|
+
for (const need of needByCid.values())
|
|
2504
|
+
if (!memo.authors.has(need.key))
|
|
2505
|
+
uncached.set(need.key, need);
|
|
2506
|
+
if (uncached.size > 0) {
|
|
2507
|
+
const addresses = new Set();
|
|
2508
|
+
const domains = new Set();
|
|
2509
|
+
for (const need of uncached.values()) {
|
|
2510
|
+
for (const address of need.karma)
|
|
2511
|
+
addresses.add(address);
|
|
2512
|
+
for (const address of need.modEdit)
|
|
2513
|
+
addresses.add(address);
|
|
2514
|
+
if (need.domain)
|
|
2515
|
+
domains.add(need.domain);
|
|
2516
|
+
}
|
|
2517
|
+
const aggregates = this._queryAuthorAggregatesByAddresses([...addresses]);
|
|
2518
|
+
const modEditRows = this._queryAuthorModEditRows([...addresses], [...domains]);
|
|
2519
|
+
for (const need of uncached.values()) {
|
|
2520
|
+
const modEditSet = new Set(need.modEdit);
|
|
2521
|
+
const modAuthorEdits = modEditRows
|
|
2522
|
+
.filter((row) => (row.targetAuthorSignerAddress !== null && modEditSet.has(row.targetAuthorSignerAddress)) ||
|
|
2523
|
+
(need.domain !== undefined && row.targetAuthorDomain === need.domain))
|
|
2524
|
+
.map((row) => row.author);
|
|
2525
|
+
const banAuthor = modAuthorEdits.find((modEdit) => typeof modEdit?.banExpiresAt === "number");
|
|
2526
|
+
const authorFlairsByMod = modAuthorEdits.find((modEdit) => modEdit?.flairs);
|
|
2527
|
+
const modEdits = {};
|
|
2528
|
+
if (banAuthor?.banExpiresAt)
|
|
2529
|
+
modEdits.banExpiresAt = banAuthor.banExpiresAt;
|
|
2530
|
+
if (authorFlairsByMod?.flairs)
|
|
2531
|
+
modEdits.flairs = authorFlairsByMod.flairs;
|
|
2532
|
+
const karma = need.karma.map((address) => aggregates.get(address)).filter((aggregate) => aggregate !== undefined);
|
|
2533
|
+
if (karma.length === 0) {
|
|
2534
|
+
memo.authors.set(need.key, Object.keys(modEdits).length > 0 ? modEdits : undefined);
|
|
2535
|
+
continue;
|
|
2536
|
+
}
|
|
2537
|
+
const last = firstBy(karma, [(aggregate) => aggregate.lastRowid, "desc"]);
|
|
2538
|
+
const first = firstBy(karma, (aggregate) => aggregate.firstRowid);
|
|
2539
|
+
memo.authors.set(need.key, {
|
|
2540
|
+
postScore: sumBy(karma, (aggregate) => aggregate.postScore),
|
|
2541
|
+
replyScore: sumBy(karma, (aggregate) => aggregate.replyScore),
|
|
2542
|
+
lastCommentCid: last.lastCommentCid,
|
|
2543
|
+
...modEdits,
|
|
2544
|
+
firstCommentTimestamp: first.firstCommentTimestamp
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
const result = new Map();
|
|
2549
|
+
for (const [cid, need] of needByCid)
|
|
2550
|
+
result.set(cid, memo.authors.get(need.key));
|
|
2551
|
+
return result;
|
|
2552
|
+
}
|
|
2553
|
+
_queryPseudonymityAliasesByCommentCids(cids) {
|
|
2554
|
+
const result = new Map();
|
|
2555
|
+
for (const { placeholders, params } of this._cidChunks(cids)) {
|
|
2556
|
+
const rows = this._prepareCached(`SELECT commentCid, aliasPrivateKey, originalAuthorPublicKey, originalAuthorName, mode, insertedAt FROM ${TABLES.PSEUDONYMITY_ALIASES} WHERE commentCid IN (${placeholders})`).all(...params);
|
|
2557
|
+
for (const row of rows)
|
|
2558
|
+
result.set(row.commentCid, row);
|
|
2559
|
+
}
|
|
2560
|
+
return result;
|
|
2561
|
+
}
|
|
2562
|
+
// What queryCommunityAuthor sums for a plain address: the address itself, every alias address of that original
|
|
2563
|
+
// author, and the original author of that alias address, batched over a chunk of addresses.
|
|
2564
|
+
_queryAliasExpandedAddressSets(addresses) {
|
|
2565
|
+
const result = new Map(addresses.map((address) => [address, new Set([address])]));
|
|
2566
|
+
for (const { placeholders, params } of this._cidChunks(addresses)) {
|
|
2567
|
+
const aliasRows = this._prepareCached(`SELECT alias.originalAuthorSignerAddress AS original, comments.authorSignerAddress AS aliasAddress
|
|
2568
|
+
FROM ${TABLES.PSEUDONYMITY_ALIASES} AS alias
|
|
2569
|
+
INNER JOIN ${TABLES.COMMENTS} AS comments ON comments.cid = alias.commentCid
|
|
2570
|
+
WHERE alias.originalAuthorSignerAddress IN (${placeholders})`).all(...params);
|
|
2571
|
+
for (const row of aliasRows)
|
|
2572
|
+
if (row.aliasAddress)
|
|
2573
|
+
result.get(row.original)?.add(row.aliasAddress);
|
|
2574
|
+
const originalRows = this._prepareCached(`SELECT comments.authorSignerAddress AS aliasAddress, alias.originalAuthorPublicKey AS originalAuthorPublicKey
|
|
2575
|
+
FROM ${TABLES.PSEUDONYMITY_ALIASES} AS alias
|
|
2576
|
+
INNER JOIN ${TABLES.COMMENTS} AS comments ON comments.cid = alias.commentCid
|
|
2577
|
+
WHERE comments.authorSignerAddress IN (${placeholders})`).all(...params);
|
|
2578
|
+
for (const row of originalRows)
|
|
2579
|
+
try {
|
|
2580
|
+
result.get(row.aliasAddress)?.add(getPKCAddressFromPublicKeySync(row.originalAuthorPublicKey));
|
|
2581
|
+
}
|
|
2582
|
+
catch {
|
|
2583
|
+
// ignore malformed keys
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
return result;
|
|
2587
|
+
}
|
|
2588
|
+
// Per address: post and reply karma (votes on the author's comments) and the rowids of the author's first and
|
|
2589
|
+
// last comments, one GROUP BY per chunk of addresses
|
|
2590
|
+
_queryAuthorAggregatesByAddresses(addresses) {
|
|
2591
|
+
const partial = new Map();
|
|
2592
|
+
for (const { placeholders, params } of this._cidChunks(addresses)) {
|
|
2593
|
+
const rows = this._prepareCached(`SELECT c.authorSignerAddress AS address,
|
|
2594
|
+
COALESCE(SUM(CASE WHEN c.depth = 0 AND v.vote = 1 THEN 1 WHEN c.depth = 0 AND v.vote = -1 THEN -1 ELSE 0 END), 0) AS postScore,
|
|
2595
|
+
COALESCE(SUM(CASE WHEN c.depth > 0 AND v.vote = 1 THEN 1 WHEN c.depth > 0 AND v.vote = -1 THEN -1 ELSE 0 END), 0) AS replyScore,
|
|
2596
|
+
MAX(c.rowid) AS lastRowid, MIN(c.rowid) AS firstRowid
|
|
2597
|
+
FROM ${TABLES.COMMENTS} c LEFT JOIN ${TABLES.VOTES} v ON c.cid = v.commentCid
|
|
2598
|
+
WHERE c.authorSignerAddress IN (${placeholders}) GROUP BY c.authorSignerAddress`).all(...params);
|
|
2599
|
+
for (const row of rows)
|
|
2600
|
+
partial.set(row.address, row);
|
|
2601
|
+
}
|
|
2602
|
+
const rowids = [...new Set([...partial.values()].flatMap((aggregate) => [aggregate.lastRowid, aggregate.firstRowid]))];
|
|
2603
|
+
const byRowid = new Map();
|
|
2604
|
+
for (const { placeholders, params } of this._cidChunks(rowids.map(String))) {
|
|
2605
|
+
const rows = this._prepareCached(`SELECT rowid, cid, timestamp FROM ${TABLES.COMMENTS} WHERE rowid IN (${placeholders})`).all(...params);
|
|
2606
|
+
for (const row of rows)
|
|
2607
|
+
byRowid.set(row.rowid, row);
|
|
2608
|
+
}
|
|
2609
|
+
const result = new Map();
|
|
2610
|
+
for (const [address, aggregate] of partial) {
|
|
2611
|
+
const last = byRowid.get(aggregate.lastRowid);
|
|
2612
|
+
const first = byRowid.get(aggregate.firstRowid);
|
|
2613
|
+
if (!last)
|
|
2614
|
+
throw Error("Failed to query communityAuthor.lastCommentCid");
|
|
2615
|
+
if (!first)
|
|
2616
|
+
throw Error("Failed to query communityAuthor.firstCommentTimestamp");
|
|
2617
|
+
result.set(address, { ...aggregate, lastCommentCid: last.cid, firstCommentTimestamp: first.timestamp });
|
|
2618
|
+
}
|
|
2619
|
+
return result;
|
|
2620
|
+
}
|
|
2621
|
+
// Every mod edit of an author (bans, flairs) targeting any of the addresses or domains, newest first, with its
|
|
2622
|
+
// target so a caller keeps the ones aimed at one author. Read by target, so a ban survives a purged comment.
|
|
2623
|
+
_queryAuthorModEditRows(addresses, domains) {
|
|
2624
|
+
const rows = [];
|
|
2625
|
+
const select = `SELECT rowid, targetAuthorSignerAddress, targetAuthorDomain, json_extract(commentModeration, '$.author') AS commentAuthorJson
|
|
2626
|
+
FROM ${TABLES.COMMENT_MODERATIONS} WHERE json_extract(commentModeration, '$.author') IS NOT NULL AND`;
|
|
2627
|
+
for (const { placeholders, params } of this._cidChunks(addresses))
|
|
2628
|
+
rows.push(...this._prepareCached(`${select} targetAuthorSignerAddress IN (${placeholders})`).all(...params));
|
|
2629
|
+
for (const { placeholders, params } of this._cidChunks(domains))
|
|
2630
|
+
rows.push(...this._prepareCached(`${select} targetAuthorDomain IN (${placeholders})`).all(...params));
|
|
2631
|
+
const unique = uniqueBy(rows, (row) => row.rowid).sort((a, b) => b.rowid - a.rowid);
|
|
2632
|
+
return unique.map((row) => ({
|
|
2633
|
+
rowid: row.rowid,
|
|
2634
|
+
targetAuthorSignerAddress: row.targetAuthorSignerAddress,
|
|
2635
|
+
targetAuthorDomain: row.targetAuthorDomain,
|
|
2636
|
+
author: JSON.parse(row.commentAuthorJson)
|
|
2637
|
+
}));
|
|
2638
|
+
}
|
|
2639
|
+
// The stored updatedAt / postUpdatesBucket / replies of a batch of comments, what calculateNewCommentUpdate reads
|
|
2640
|
+
// before writing the next CommentUpdate (issue #352)
|
|
2641
|
+
queryCommentUpdateTimestampBucketRepliesByCids(cids) {
|
|
2642
|
+
const result = new Map();
|
|
2643
|
+
for (const { placeholders, params } of this._cidChunks(cids)) {
|
|
2644
|
+
const rows = this._prepareCached(`SELECT cid, updatedAt, postUpdatesBucket, replies FROM ${TABLES.COMMENT_UPDATES} WHERE cid IN (${placeholders})`).all(...params);
|
|
2645
|
+
for (const row of rows)
|
|
2646
|
+
result.set(row.cid, {
|
|
2647
|
+
updatedAt: row.updatedAt,
|
|
2648
|
+
postUpdatesBucket: row.postUpdatesBucket ?? undefined,
|
|
2649
|
+
replies: typeof row.replies === "string" ? JSON.parse(row.replies) : undefined
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
return result;
|
|
2275
2653
|
}
|
|
2276
2654
|
queryLatestPostCid() {
|
|
2277
|
-
return this.
|
|
2278
|
-
.prepare(`SELECT c.cid FROM ${TABLES.COMMENTS} c
|
|
2655
|
+
return this._prepareCached(`SELECT c.cid FROM ${TABLES.COMMENTS} c
|
|
2279
2656
|
LEFT JOIN ${TABLES.COMMENT_UPDATES} cu ON cu.cid = c.cid
|
|
2280
2657
|
WHERE c.depth = 0
|
|
2281
2658
|
AND c.pendingApproval IS NOT 1
|
|
2282
2659
|
AND COALESCE(cu.approved, 1) != 0
|
|
2283
2660
|
ORDER BY c.rowid DESC
|
|
2284
|
-
LIMIT 1`)
|
|
2285
|
-
.get();
|
|
2661
|
+
LIMIT 1`).get();
|
|
2286
2662
|
}
|
|
2287
2663
|
queryLatestCommentCid() {
|
|
2288
|
-
return this.
|
|
2289
|
-
.prepare(`SELECT c.cid FROM ${TABLES.COMMENTS} c
|
|
2664
|
+
return this._prepareCached(`SELECT c.cid FROM ${TABLES.COMMENTS} c
|
|
2290
2665
|
LEFT JOIN ${TABLES.COMMENT_UPDATES} cu ON cu.cid = c.cid
|
|
2291
2666
|
WHERE c.pendingApproval IS NOT 1
|
|
2292
2667
|
AND COALESCE(cu.approved, 1) != 0
|
|
2293
2668
|
ORDER BY c.rowid DESC
|
|
2294
|
-
LIMIT 1`)
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2669
|
+
LIMIT 1`).get();
|
|
2670
|
+
}
|
|
2671
|
+
// Every comment row in rowid order, one batch at a time, so a caller that walks the whole board holds a
|
|
2672
|
+
// batch instead of the board. Reading them all at once (what the repin on start used to do) was 2.7 GB of
|
|
2673
|
+
// rows on a 3.1M-comment board, an OOM before the publish loop had started (issue #359).
|
|
2674
|
+
//
|
|
2675
|
+
// Keyset paging on rowid rather than one held-open cursor: the caller awaits IPFS between batches, and an
|
|
2676
|
+
// open better-sqlite3 iterator would block every other statement on this connection for that whole time.
|
|
2677
|
+
// The rowid selected alongside the row is the cursor only: it is not part of the comments schema, so the
|
|
2678
|
+
// row parser drops it.
|
|
2679
|
+
*iterateAllCommentsOrderedByIdAsc(batchSize) {
|
|
2680
|
+
assert(Number.isInteger(batchSize) && batchSize > 0, `iterateAllCommentsOrderedByIdAsc needs a positive batch size`);
|
|
2681
|
+
let lastRowid = 0;
|
|
2682
|
+
while (true) {
|
|
2683
|
+
const rows = this._prepareCached(`SELECT rowid, * FROM ${TABLES.COMMENTS} WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`).all(lastRowid, batchSize);
|
|
2684
|
+
if (rows.length === 0)
|
|
2685
|
+
return;
|
|
2686
|
+
lastRowid = rows[rows.length - 1].rowid;
|
|
2687
|
+
yield rows.map((row) => this._parseCommentsTableRow(row));
|
|
2688
|
+
}
|
|
2300
2689
|
}
|
|
2301
2690
|
queryAuthorModEdits(opts) {
|
|
2302
2691
|
const { authorSignerAddresses, authorDomain } = opts;
|
|
@@ -2314,12 +2703,10 @@ export class DbHandler {
|
|
|
2314
2703
|
params.push(authorDomain);
|
|
2315
2704
|
}
|
|
2316
2705
|
// Query directly by targetAuthorSignerAddress or targetAuthorDomain to find bans/flairs even for purged comments
|
|
2317
|
-
const modAuthorEditsRaw = this.
|
|
2318
|
-
.prepare(`
|
|
2706
|
+
const modAuthorEditsRaw = this._prepareCached(`
|
|
2319
2707
|
SELECT json_extract(commentModeration, '$.author') AS commentAuthorJson FROM ${TABLES.COMMENT_MODERATIONS}
|
|
2320
2708
|
WHERE (${conditions.join(" OR ")}) AND json_extract(commentModeration, '$.author') IS NOT NULL ORDER BY rowid DESC
|
|
2321
|
-
`)
|
|
2322
|
-
.all(...params);
|
|
2709
|
+
`).all(...params);
|
|
2323
2710
|
const modAuthorEdits = modAuthorEditsRaw.map((r) => JSON.parse(r.commentAuthorJson));
|
|
2324
2711
|
const banAuthor = modAuthorEdits.find((modEdit) => typeof modEdit?.banExpiresAt === "number");
|
|
2325
2712
|
const authorFlairsByMod = modAuthorEdits.find((modEdit) => modEdit?.flairs);
|
|
@@ -2370,37 +2757,24 @@ export class DbHandler {
|
|
|
2370
2757
|
}
|
|
2371
2758
|
queryCommunityAuthor(authorSignerAddress, authorDomain) {
|
|
2372
2759
|
const authorSignerAddresses = new Set([authorSignerAddress]);
|
|
2373
|
-
// If the provided address is the original signer, include all alias signer addresses for that author
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
SELECT
|
|
2760
|
+
// If the provided address is the original signer, include all alias signer addresses for that author
|
|
2761
|
+
// (originalAuthorSignerAddress is derived from the original public key at insert and indexed, issue #351).
|
|
2762
|
+
const aliasSignerRows = this._prepareCached(`
|
|
2763
|
+
SELECT comments.authorSignerAddress
|
|
2377
2764
|
FROM ${TABLES.PSEUDONYMITY_ALIASES} AS alias
|
|
2378
|
-
|
|
2379
|
-
.
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
const commentRow = this._db
|
|
2385
|
-
.prepare(`SELECT authorSignerAddress FROM ${TABLES.COMMENTS} WHERE cid = ?`)
|
|
2386
|
-
.get(aliasRow.commentCid);
|
|
2387
|
-
if (commentRow?.authorSignerAddress)
|
|
2388
|
-
authorSignerAddresses.add(commentRow.authorSignerAddress);
|
|
2389
|
-
}
|
|
2390
|
-
}
|
|
2391
|
-
catch {
|
|
2392
|
-
// ignore malformed keys
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2765
|
+
INNER JOIN ${TABLES.COMMENTS} AS comments ON comments.cid = alias.commentCid
|
|
2766
|
+
WHERE alias.originalAuthorSignerAddress = ?
|
|
2767
|
+
`).all(authorSignerAddress);
|
|
2768
|
+
for (const row of aliasSignerRows)
|
|
2769
|
+
if (row.authorSignerAddress)
|
|
2770
|
+
authorSignerAddresses.add(row.authorSignerAddress);
|
|
2395
2771
|
// If the provided address is an alias, include the original signer address for that alias.
|
|
2396
|
-
const aliasRowsForAliasAddress = this.
|
|
2397
|
-
.prepare(`
|
|
2772
|
+
const aliasRowsForAliasAddress = this._prepareCached(`
|
|
2398
2773
|
SELECT alias.originalAuthorPublicKey
|
|
2399
2774
|
FROM ${TABLES.PSEUDONYMITY_ALIASES} AS alias
|
|
2400
2775
|
INNER JOIN ${TABLES.COMMENTS} AS comments ON comments.cid = alias.commentCid
|
|
2401
2776
|
WHERE comments.authorSignerAddress = ?
|
|
2402
|
-
`)
|
|
2403
|
-
.all(authorSignerAddress);
|
|
2777
|
+
`).all(authorSignerAddress);
|
|
2404
2778
|
for (const aliasRow of aliasRowsForAliasAddress) {
|
|
2405
2779
|
try {
|
|
2406
2780
|
const originalAddress = getPKCAddressFromPublicKeySync(aliasRow.originalAuthorPublicKey);
|
|
@@ -2418,15 +2792,13 @@ export class DbHandler {
|
|
|
2418
2792
|
return undefined;
|
|
2419
2793
|
const placeholders = karmaAddresses.map(() => "?").join(", ");
|
|
2420
2794
|
const modAuthorEdits = this.queryAuthorModEdits({ authorSignerAddresses: modEditAddresses, authorDomain });
|
|
2421
|
-
const authorCommentsData = this.
|
|
2422
|
-
.prepare(`
|
|
2795
|
+
const authorCommentsData = this._prepareCached(`
|
|
2423
2796
|
SELECT c.depth, c.rowid, c.timestamp, c.cid,
|
|
2424
2797
|
COALESCE(SUM(CASE WHEN v.vote = 1 THEN 1 ELSE 0 END), 0) as upvoteCount,
|
|
2425
2798
|
COALESCE(SUM(CASE WHEN v.vote = -1 THEN 1 ELSE 0 END), 0) as downvoteCount
|
|
2426
2799
|
FROM ${TABLES.COMMENTS} c LEFT JOIN ${TABLES.VOTES} v ON c.cid = v.commentCid
|
|
2427
2800
|
WHERE c.authorSignerAddress IN (${placeholders}) GROUP BY c.cid
|
|
2428
|
-
`)
|
|
2429
|
-
.all(...karmaAddresses);
|
|
2801
|
+
`).all(...karmaAddresses);
|
|
2430
2802
|
if (authorCommentsData.length === 0) {
|
|
2431
2803
|
if (Object.keys(modAuthorEdits).length > 0) {
|
|
2432
2804
|
return modAuthorEdits;
|
|
@@ -2456,30 +2828,6 @@ export class DbHandler {
|
|
|
2456
2828
|
* We query karma for ONLY the alias address (no lookup to other aliases like queryCommunityAuthor does),
|
|
2457
2829
|
* but include mod edits from both alias and original author.
|
|
2458
2830
|
*/
|
|
2459
|
-
queryCommunityAuthorForCommentUpdate(opts) {
|
|
2460
|
-
const { authorSignerAddress, commentCid, authorDomain } = opts;
|
|
2461
|
-
// Check if this comment has a pseudonymity alias
|
|
2462
|
-
const aliasRow = this.queryPseudonymityAliasByCommentCid(commentCid);
|
|
2463
|
-
if (!aliasRow) {
|
|
2464
|
-
// No pseudonymity mode - use standard aggregated karma
|
|
2465
|
-
return this.queryCommunityAuthor(authorSignerAddress, authorDomain);
|
|
2466
|
-
}
|
|
2467
|
-
// Get original author's address for mod edits (bans/flairs are applied to original author)
|
|
2468
|
-
const modEditAddresses = [authorSignerAddress];
|
|
2469
|
-
try {
|
|
2470
|
-
const originalAddress = getPKCAddressFromPublicKeySync(aliasRow.originalAuthorPublicKey);
|
|
2471
|
-
if (originalAddress !== authorSignerAddress) {
|
|
2472
|
-
modEditAddresses.push(originalAddress);
|
|
2473
|
-
}
|
|
2474
|
-
}
|
|
2475
|
-
catch {
|
|
2476
|
-
// ignore malformed keys
|
|
2477
|
-
}
|
|
2478
|
-
// For mod edits (bans/flairs), use the original author's domain if available
|
|
2479
|
-
const modEditDomain = aliasRow.originalAuthorName || authorDomain;
|
|
2480
|
-
// Query karma for just this alias, but mod edits from both alias and original
|
|
2481
|
-
return this._queryCommunityAuthorByAddresses([authorSignerAddress], modEditAddresses, modEditDomain);
|
|
2482
|
-
}
|
|
2483
2831
|
_getAllDescendantCids(cid) {
|
|
2484
2832
|
const allCids = [cid];
|
|
2485
2833
|
const directChildren = this._db.prepare(`SELECT cid FROM ${TABLES.COMMENTS} WHERE parentCid = ?`).all(cid);
|
|
@@ -2489,6 +2837,7 @@ export class DbHandler {
|
|
|
2489
2837
|
return allCids;
|
|
2490
2838
|
}
|
|
2491
2839
|
purgeComment(cid, isNestedCall = false) {
|
|
2840
|
+
this._publicationWriteGeneration++;
|
|
2492
2841
|
const log = Logger("pkc-js:local-community:db-handler:purgeComment");
|
|
2493
2842
|
const purgedRecords = [];
|
|
2494
2843
|
const detachedPageCids = [];
|
|
@@ -2560,10 +2909,10 @@ export class DbHandler {
|
|
|
2560
2909
|
const allCommentsToUpdate = [...allAffectedAuthorCids, ...Array.from(commentsToForceUpdate)];
|
|
2561
2910
|
// Force update on a random comment to ensure IPNS update triggers even if no other comments need updating
|
|
2562
2911
|
// Make sure we don't select a comment that's being purged
|
|
2563
|
-
|
|
2912
|
+
// The purged set as one JSON parameter: a purged post can have more descendants than a statement may bind
|
|
2564
2913
|
const randomComment = this._db
|
|
2565
|
-
.prepare(`SELECT cid FROM ${TABLES.COMMENTS} WHERE cid NOT IN (
|
|
2566
|
-
.get(
|
|
2914
|
+
.prepare(`SELECT cid FROM ${TABLES.COMMENTS} WHERE cid NOT IN (SELECT value FROM json_each(?)) ORDER BY RANDOM() LIMIT 1`)
|
|
2915
|
+
.get(JSON.stringify(allCidsToBeDeleted));
|
|
2567
2916
|
if (randomComment) {
|
|
2568
2917
|
allCommentsToUpdate.push(randomComment.cid);
|
|
2569
2918
|
log(`Forcing update on random comment ${randomComment.cid} to ensure IPNS update after purge`);
|
|
@@ -2701,11 +3050,12 @@ export class DbHandler {
|
|
|
2701
3050
|
const communityDbPath = path.join(this._community._pkc.dataPath, "communities", this._community.address);
|
|
2702
3051
|
return fs.existsSync(communityDbPath);
|
|
2703
3052
|
}
|
|
3053
|
+
// Batched under the SQLite variable cap: a cycle that updates every comment (after a migration, or a
|
|
3054
|
+
// settings.pages edit) passes more cids than one statement may bind (issue #351)
|
|
2704
3055
|
markCommentsAsPublishedToPostUpdates(commentCids) {
|
|
2705
3056
|
if (commentCids.length === 0)
|
|
2706
3057
|
return;
|
|
2707
|
-
|
|
2708
|
-
stmt.run(...commentCids);
|
|
3058
|
+
this._runForEachCidBatch(commentCids, (placeholders) => `UPDATE ${TABLES.COMMENT_UPDATES} SET publishedToPostUpdatesMFS = 1 WHERE cid IN (${placeholders})`);
|
|
2709
3059
|
}
|
|
2710
3060
|
forceUpdateOnAllComments() {
|
|
2711
3061
|
this._db.prepare(`UPDATE ${TABLES.COMMENT_UPDATES} SET publishedToPostUpdatesMFS = 0`).run();
|
|
@@ -2713,92 +3063,64 @@ export class DbHandler {
|
|
|
2713
3063
|
forceUpdateOnAllCommentsWithCid(commentCids) {
|
|
2714
3064
|
if (commentCids.length === 0)
|
|
2715
3065
|
return;
|
|
2716
|
-
this.
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
const
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
LEFT JOIN ${TABLES.COMMENT_UPDATES} cu ON c.cid = cu.cid`)
|
|
2726
|
-
.all();
|
|
2727
|
-
return rows.map((row) => {
|
|
2728
|
-
const allPageCids = [];
|
|
2729
|
-
if (typeof row.replies === "string" && row.replies.length > 0) {
|
|
2730
|
-
try {
|
|
2731
|
-
const parsed = JSON.parse(row.replies);
|
|
2732
|
-
for (const sortEntry of Object.values(parsed)) {
|
|
2733
|
-
if (sortEntry?.allPageCids)
|
|
2734
|
-
allPageCids.push(...sortEntry.allPageCids);
|
|
2735
|
-
}
|
|
2736
|
-
}
|
|
2737
|
-
catch (e) {
|
|
2738
|
-
log.error(`Failed to parse replies JSON for comment ${row.cid} when collecting cids`, e);
|
|
2739
|
-
}
|
|
3066
|
+
this._runForEachCidBatch(commentCids, (placeholders) => `UPDATE ${TABLES.COMMENT_UPDATES} SET publishedToPostUpdatesMFS = 0 WHERE cid IN (${placeholders})`);
|
|
3067
|
+
}
|
|
3068
|
+
// The write form of _forEachCidBatch: one statement per batch, in one transaction
|
|
3069
|
+
_runForEachCidBatch(cids, queryFor) {
|
|
3070
|
+
const BATCH = 4096;
|
|
3071
|
+
const run = this._db.transaction(() => {
|
|
3072
|
+
for (let start = 0; start < cids.length; start += BATCH) {
|
|
3073
|
+
const batch = cids.slice(start, start + BATCH);
|
|
3074
|
+
this._prepareCached(queryFor(new Array(batch.length).fill("?").join(","))).run(...batch);
|
|
2740
3075
|
}
|
|
2741
|
-
return { cid: row.cid, allPageCids };
|
|
2742
3076
|
});
|
|
3077
|
+
run();
|
|
3078
|
+
}
|
|
3079
|
+
// Every comment's cid and the cids of its reply pages, in rowid slices: what delete() unpins. One slice is
|
|
3080
|
+
// held at a time, so deleting a board of a million comments costs a slice of memory, not the board
|
|
3081
|
+
// (issue #389: the all-at-once array and Set ran a 4 GB heap out at 1M).
|
|
3082
|
+
*iterateCommentCidsAndTheirPageCids(sliceSize) {
|
|
3083
|
+
const log = Logger("pkc-js:local-community:db-handler:iterateCommentCidsAndTheirPageCids");
|
|
3084
|
+
const statement = this._db.prepare(`SELECT c.rowid AS rowid, c.cid AS cid, cu.replies AS replies
|
|
3085
|
+
FROM ${TABLES.COMMENTS} c
|
|
3086
|
+
LEFT JOIN ${TABLES.COMMENT_UPDATES} cu ON c.cid = cu.cid
|
|
3087
|
+
WHERE c.rowid > ?
|
|
3088
|
+
ORDER BY c.rowid ASC
|
|
3089
|
+
LIMIT ?`);
|
|
3090
|
+
let afterRowid = 0;
|
|
3091
|
+
while (true) {
|
|
3092
|
+
const rows = statement.all(afterRowid, sliceSize);
|
|
3093
|
+
if (rows.length === 0)
|
|
3094
|
+
return;
|
|
3095
|
+
afterRowid = rows[rows.length - 1].rowid;
|
|
3096
|
+
yield rows.map((row) => {
|
|
3097
|
+
const allPageCids = [];
|
|
3098
|
+
if (typeof row.replies === "string" && row.replies.length > 0) {
|
|
3099
|
+
try {
|
|
3100
|
+
const parsed = JSON.parse(row.replies);
|
|
3101
|
+
for (const sortEntry of Object.values(parsed)) {
|
|
3102
|
+
if (sortEntry?.allPageCids)
|
|
3103
|
+
allPageCids.push(...sortEntry.allPageCids);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
catch (e) {
|
|
3107
|
+
log.error(`Failed to parse replies JSON for comment ${row.cid} when collecting cids`, e);
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
return { cid: row.cid, allPageCids };
|
|
3111
|
+
});
|
|
3112
|
+
}
|
|
2743
3113
|
}
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
const
|
|
2748
|
-
const
|
|
2749
|
-
if (pageOptions.excludeCommentsWithDifferentCommunityAddress)
|
|
2750
|
-
activeScoreRootConditions.push(rootAddrClause);
|
|
2751
|
-
if (pageOptions.excludeCommentPendingApproval)
|
|
2752
|
-
activeScoreRootConditions.push(this._pendingApprovalClause("p"));
|
|
2753
|
-
if (pageOptions.excludeRemovedComments)
|
|
2754
|
-
activeScoreRootConditions.push(this._removedClause("cu_root"));
|
|
2755
|
-
if (pageOptions.excludeDeletedComments)
|
|
2756
|
-
activeScoreRootConditions.push(this._deletedFromUpdatesClause("cu_root"));
|
|
2757
|
-
if (pageOptions.excludeCommentWithApprovedFalse)
|
|
2758
|
-
activeScoreRootConditions.push(this._approvedClause("cu_root"));
|
|
2759
|
-
const activeScoreRootWhere = `WHERE ${activeScoreRootConditions.join(" AND ")}`;
|
|
2760
|
-
const activeScoreDescendantConditions = [];
|
|
2761
|
-
if (pageOptions.excludeCommentsWithDifferentCommunityAddress)
|
|
2762
|
-
activeScoreDescendantConditions.push(descAddrClause);
|
|
2763
|
-
if (pageOptions.excludeCommentPendingApproval)
|
|
2764
|
-
activeScoreDescendantConditions.push(this._pendingApprovalClause("c"));
|
|
2765
|
-
if (pageOptions.excludeRemovedComments)
|
|
2766
|
-
activeScoreDescendantConditions.push(this._removedClause("cu"));
|
|
2767
|
-
if (pageOptions.excludeDeletedComments)
|
|
2768
|
-
activeScoreDescendantConditions.push(this._deletedFromLookupClause("deleted_lookup"));
|
|
2769
|
-
if (pageOptions.excludeCommentWithApprovedFalse)
|
|
2770
|
-
activeScoreDescendantConditions.push(this._approvedClause("cu"));
|
|
2771
|
-
const activeScoreDescendantWhere = activeScoreDescendantConditions.length > 0 ? `WHERE ${activeScoreDescendantConditions.join(" AND ")}` : "";
|
|
2772
|
-
const activeScoresCte = `
|
|
2773
|
-
WITH RECURSIVE descendants AS (
|
|
2774
|
-
SELECT p.cid AS post_cid, p.cid AS current_cid, p.timestamp
|
|
2775
|
-
FROM ${TABLES.COMMENTS} p
|
|
2776
|
-
INNER JOIN ${TABLES.COMMENT_UPDATES} cu_root ON p.cid = cu_root.cid
|
|
2777
|
-
${activeScoreRootWhere}
|
|
2778
|
-
UNION ALL
|
|
2779
|
-
SELECT desc_tree.post_cid, c.cid AS current_cid, c.timestamp FROM ${TABLES.COMMENTS} c
|
|
2780
|
-
INNER JOIN ${TABLES.COMMENT_UPDATES} cu ON c.cid = cu.cid
|
|
2781
|
-
LEFT JOIN (SELECT cid, json_extract(edit, '$.deleted') AS deleted_flag FROM ${TABLES.COMMENT_UPDATES}) AS deleted_lookup ON c.cid = deleted_lookup.cid
|
|
2782
|
-
JOIN descendants desc_tree ON c.parentCid = desc_tree.current_cid
|
|
2783
|
-
${activeScoreDescendantWhere}
|
|
2784
|
-
) SELECT post_cid, MAX(timestamp) as active_score FROM descendants GROUP BY post_cid
|
|
2785
|
-
`;
|
|
2786
|
-
const commentUpdateCols = keys(pageOptions.commentUpdateFieldsToExclude
|
|
2787
|
-
? omit(CommentUpdateSchema.shape, pageOptions.commentUpdateFieldsToExclude)
|
|
2788
|
-
: CommentUpdateSchema.shape);
|
|
2789
|
-
const commentUpdateSelects = commentUpdateCols.map((col) => `cu.${col} AS commentUpdate_${col}`);
|
|
2790
|
-
const commentIpfsCols = [...keys(CommentIpfsSchema.shape), "extraProps"];
|
|
2791
|
-
const commentIpfsSelects = commentIpfsCols.map((col) => `c.${col} AS commentIpfs_${col}`);
|
|
2792
|
-
let postsQueryStr = `
|
|
2793
|
-
SELECT ${commentIpfsSelects.join(", ")}, ${commentUpdateSelects.join(", ")}, asc_scores.active_score
|
|
2794
|
-
FROM ${TABLES.COMMENTS} c INNER JOIN ${TABLES.COMMENT_UPDATES} cu ON c.cid = cu.cid
|
|
2795
|
-
INNER JOIN (${activeScoresCte}) AS asc_scores ON c.cid = asc_scores.post_cid
|
|
2796
|
-
`;
|
|
2797
|
-
const params = { ...rootAddrParams, ...descAddrParams };
|
|
3114
|
+
// Every post (depth 0) that survives the exclusion options, with its DB-format replies (CID refs) still attached;
|
|
3115
|
+
// callers resolve those with resolveRepliesCidRefsForEntries. Scoring is the page sort's job (issue #73).
|
|
3116
|
+
queryPosts(pageOptions) {
|
|
3117
|
+
const mapper = this._pageEntryMapper(pageOptions.commentUpdateFieldsToExclude);
|
|
3118
|
+
const params = {};
|
|
2798
3119
|
const postsWhereClauses = ["c.depth = 0"];
|
|
2799
3120
|
if (pageOptions.excludeCommentsWithDifferentCommunityAddress) {
|
|
2800
|
-
|
|
2801
|
-
|
|
3121
|
+
const { clause, params: addrParams } = this._communityAddressClauseNamed("c", "asPosts");
|
|
3122
|
+
postsWhereClauses.push(clause);
|
|
3123
|
+
Object.assign(params, addrParams);
|
|
2802
3124
|
}
|
|
2803
3125
|
if (pageOptions.excludeRemovedComments)
|
|
2804
3126
|
postsWhereClauses.push(this._removedClause("cu"));
|
|
@@ -2808,16 +3130,111 @@ export class DbHandler {
|
|
|
2808
3130
|
postsWhereClauses.push(this._pendingApprovalClause("c"));
|
|
2809
3131
|
if (pageOptions.excludeCommentWithApprovedFalse)
|
|
2810
3132
|
postsWhereClauses.push(this._approvedClause("cu"));
|
|
2811
|
-
postsQueryStr
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
3133
|
+
const postsQueryStr = `
|
|
3134
|
+
SELECT ${mapper.selectList((col) => `c.${col}`, (col) => `cu.${col}`)}
|
|
3135
|
+
FROM ${TABLES.COMMENTS} c INNER JOIN ${TABLES.COMMENT_UPDATES} cu ON c.cid = cu.cid
|
|
3136
|
+
WHERE ${postsWhereClauses.join(" AND ")}
|
|
3137
|
+
`;
|
|
3138
|
+
const rows = this._prepareCached(postsQueryStr).raw(true).all(params);
|
|
3139
|
+
return rows.map(mapper.map);
|
|
3140
|
+
}
|
|
3141
|
+
// queryPosts plus the built-in active score: the bump time the CommentUpdate carries (max of the post's own
|
|
3142
|
+
// timestamp and lastReplyTimestamp), the same number the active page sort file and a client compute. Kept as one
|
|
3143
|
+
// entry point for "posts with their bump score" for the migration tests.
|
|
3144
|
+
queryPostsWithActiveScore(pageOptions) {
|
|
3145
|
+
return this.queryPosts(pageOptions).map((post) => ({
|
|
3146
|
+
...post,
|
|
3147
|
+
activeScore: Math.max(post.comment.timestamp, post.commentUpdate.lastReplyTimestamp ?? 0)
|
|
3148
|
+
}));
|
|
3149
|
+
}
|
|
3150
|
+
// Every reply (depth > 0) in the community, or under one post, with no exclusions applied: the one reply set a
|
|
3151
|
+
// generation loads for its requireReplies sorts, each of which then filters it with its own exclusion options
|
|
3152
|
+
// through applyPageSortExclusions (the same filter a client applies) and slices each comment's subtree from it
|
|
3153
|
+
// (createDescendantsLookup). Lean on purpose (PageSortReplyEntry): a fixed column list mapped by hand, no
|
|
3154
|
+
// signature, no nested pages, no schema row parser, since a board can hold a million replies and this runs per
|
|
3155
|
+
// generation. A pending-approval reply is marked on its CommentUpdate the way the mod queue marks one, so the
|
|
3156
|
+
// filter can see it; the list never reaches a page.
|
|
3157
|
+
queryAllRepliesForPageSort(opts = {}) {
|
|
3158
|
+
// A list of posts streams in batches under the SQLite variable cap: what a post sort with requireReplies
|
|
3159
|
+
// scores over, one batch of posts' subtrees at a time (issue #351)
|
|
3160
|
+
if (opts.postCids) {
|
|
3161
|
+
const entries = [];
|
|
3162
|
+
const BATCH = 4096;
|
|
3163
|
+
for (let start = 0; start < opts.postCids.length; start += BATCH) {
|
|
3164
|
+
const batch = opts.postCids.slice(start, start + BATCH);
|
|
3165
|
+
entries.push(...this._queryRepliesForPageSort(`c.postCid IN (${batch.map(() => "?").join(",")})`, batch));
|
|
3166
|
+
}
|
|
3167
|
+
return entries;
|
|
3168
|
+
}
|
|
3169
|
+
if (opts.postCid)
|
|
3170
|
+
return this._queryRepliesForPageSort("c.postCid = ?", [opts.postCid]);
|
|
3171
|
+
return this._queryRepliesForPageSort("1 = 1", []);
|
|
3172
|
+
}
|
|
3173
|
+
_queryRepliesForPageSort(scopeClause, params) {
|
|
3174
|
+
const whereClauses = ["c.depth > 0", scopeClause];
|
|
3175
|
+
const query = `
|
|
3176
|
+
SELECT c.cid, c.parentCid, c.postCid, c.depth, c.timestamp, c.content, c.title, c.link, c.author, c.communityPublicKey,
|
|
3177
|
+
c.communityName, c.nsfw, c.spoiler, c.flairs, c.pendingApproval,
|
|
3178
|
+
cu.upvoteCount, cu.downvoteCount, cu.replyCount, cu.childCount, cu.updatedAt, cu.lastReplyTimestamp, cu.pinned,
|
|
3179
|
+
cu.locked, cu.removed, cu.approved, cu.nsfw AS cuNsfw, cu.spoiler AS cuSpoiler, cu.flairs AS cuFlairs,
|
|
3180
|
+
json_extract(cu.edit, '$.deleted') AS editDeleted
|
|
3181
|
+
FROM ${TABLES.COMMENTS} c INNER JOIN ${TABLES.COMMENT_UPDATES} cu ON c.cid = cu.cid
|
|
3182
|
+
WHERE ${whereClauses.join(" AND ")}
|
|
3183
|
+
`;
|
|
3184
|
+
const rows = this._prepareCached(query)
|
|
3185
|
+
.raw(true)
|
|
3186
|
+
.all(...params);
|
|
3187
|
+
const entries = new Array(rows.length);
|
|
3188
|
+
for (let i = 0; i < rows.length; i++) {
|
|
3189
|
+
const [cid, parentCid, postCid, depth, timestamp, content, title, link, author, communityPublicKey, communityName, nsfw, spoiler, flairs, pendingApproval, upvoteCount, downvoteCount, replyCount, childCount, updatedAt, lastReplyTimestamp, pinned, locked, removed, approved, cuNsfw, cuSpoiler, cuFlairs, editDeleted] = rows[i];
|
|
3190
|
+
const comment = { parentCid, postCid, depth, timestamp, author: JSON.parse(author) };
|
|
3191
|
+
if (content !== null)
|
|
3192
|
+
comment.content = content;
|
|
3193
|
+
if (title !== null)
|
|
3194
|
+
comment.title = title;
|
|
3195
|
+
if (link !== null)
|
|
3196
|
+
comment.link = link;
|
|
3197
|
+
if (communityPublicKey !== null)
|
|
3198
|
+
comment.communityPublicKey = communityPublicKey;
|
|
3199
|
+
if (communityName !== null)
|
|
3200
|
+
comment.communityName = communityName;
|
|
3201
|
+
if (nsfw !== null)
|
|
3202
|
+
comment.nsfw = Boolean(nsfw);
|
|
3203
|
+
if (spoiler !== null)
|
|
3204
|
+
comment.spoiler = Boolean(spoiler);
|
|
3205
|
+
if (flairs !== null)
|
|
3206
|
+
comment.flairs = JSON.parse(flairs);
|
|
3207
|
+
const commentUpdate = {
|
|
3208
|
+
cid,
|
|
3209
|
+
upvoteCount,
|
|
3210
|
+
downvoteCount,
|
|
3211
|
+
replyCount,
|
|
3212
|
+
childCount,
|
|
3213
|
+
updatedAt
|
|
2819
3214
|
};
|
|
2820
|
-
|
|
3215
|
+
if (lastReplyTimestamp !== null)
|
|
3216
|
+
commentUpdate.lastReplyTimestamp = lastReplyTimestamp;
|
|
3217
|
+
if (pinned !== null)
|
|
3218
|
+
commentUpdate.pinned = Boolean(pinned);
|
|
3219
|
+
if (locked !== null)
|
|
3220
|
+
commentUpdate.locked = Boolean(locked);
|
|
3221
|
+
if (removed !== null)
|
|
3222
|
+
commentUpdate.removed = Boolean(removed);
|
|
3223
|
+
if (approved !== null)
|
|
3224
|
+
commentUpdate.approved = Boolean(approved);
|
|
3225
|
+
if (cuNsfw !== null)
|
|
3226
|
+
commentUpdate.nsfw = Boolean(cuNsfw);
|
|
3227
|
+
if (cuSpoiler !== null)
|
|
3228
|
+
commentUpdate.spoiler = Boolean(cuSpoiler);
|
|
3229
|
+
if (cuFlairs !== null)
|
|
3230
|
+
commentUpdate.flairs = JSON.parse(cuFlairs);
|
|
3231
|
+
if (editDeleted)
|
|
3232
|
+
commentUpdate.edit = { deleted: true };
|
|
3233
|
+
if (pendingApproval)
|
|
3234
|
+
commentUpdate.pendingApproval = true;
|
|
3235
|
+
entries[i] = { comment, commentUpdate };
|
|
3236
|
+
}
|
|
3237
|
+
return entries;
|
|
2821
3238
|
}
|
|
2822
3239
|
_processRecordsForDbBeforeInsert(records) {
|
|
2823
3240
|
return records.map((record) => {
|