chore: publish pearcord-scheduled-messages from pearcord workspace
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
# pearcord-scheduled-messages
|
||||
|
||||
Guild + DM scheduled message queue persistence for Pearcord.
|
||||
|
||||
## Overview
|
||||
|
||||
Phase 674 module wrapping HyperDB `@pearcord/scheduled-messages` with JsonStore fallback. Metadata is gossiped on the guild mesh; message body is sent only at fire time.
|
||||
|
||||
## Exports
|
||||
|
||||
| Symbol | Purpose |
|
||||
|--------|---------|
|
||||
| `ScheduledMessageStore` | Queue CRUD, capacity checks, list by guild/channel |
|
||||
|
||||
## Policy
|
||||
|
||||
Uses `pearcord-shared/dm-scheduled-policy` for recurrence computation and queue caps (per channel / per guild).
|
||||
|
||||
## Platform integration
|
||||
|
||||
- `pearcord-platform/polls-scheduling.js` — `scheduleMessage`, `updateScheduledMessage`, `cancelScheduledMessage`, `sendNowScheduledMessage`, due runner, gossip + heal
|
||||
- View fields: `scheduledQueue`, `scheduledQueueCount`
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npm run test:phase674-polls-scheduling
|
||||
PEARCORD_SKIP_MESH_ROUNDTRIP=1 npm run test:agentctl-phase674-polls-scheduling
|
||||
```
|
||||
|
||||
See [SCHEDULED_MESSAGES.md](../../docs/SCHEDULED_MESSAGES.md).
|
||||
@@ -0,0 +1,143 @@
|
||||
'use strict'
|
||||
|
||||
const path = require('bare-path')
|
||||
const { JsonStore } = require('pearcord-db/store-json')
|
||||
const { COLLECTIONS } = require('pearcord-shared')
|
||||
const {
|
||||
DM_SCHEDULE_MAX_QUEUED_PER_CHANNEL,
|
||||
DM_SCHEDULE_STALE_RETENTION_MS
|
||||
} = require('pearcord-shared/dm-scheduled-policy')
|
||||
|
||||
const GUILD_SCHEDULE_MAX_QUEUED_PER_CHANNEL = DM_SCHEDULE_MAX_QUEUED_PER_CHANNEL
|
||||
const GUILD_SCHEDULE_MAX_QUEUED_PER_GUILD = 100
|
||||
|
||||
class ScheduledMessageStore {
|
||||
constructor (opts = {}) {
|
||||
this.db = opts.db || null
|
||||
this.storagePath = opts.storagePath || './pearcord-storage'
|
||||
this.collection = COLLECTIONS.SCHEDULED_MESSAGES
|
||||
this._json = opts.jsonStore || new JsonStore(path.join(this.storagePath, 'scheduled-messages'))
|
||||
this._hyperDbReady = false
|
||||
}
|
||||
|
||||
get engine () {
|
||||
return this.db?.getEngine?.() === 'hyperdb' && this._hyperDbReady ? 'hyperdb' : 'json'
|
||||
}
|
||||
|
||||
async ready () {
|
||||
await this._json.ready()
|
||||
if (this.db) {
|
||||
await this.db.ready()
|
||||
if (this.db.getEngine?.() === 'hyperdb') {
|
||||
try {
|
||||
await this.db.get(this.collection, { guildId: '__probe__', id: '__probe__' })
|
||||
this._hyperDbReady = true
|
||||
} catch (err) {
|
||||
if (/Unknown index|Unknown collection/.test(err.message)) this._hyperDbReady = false
|
||||
else this._hyperDbReady = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
_disableHyperDb (err) {
|
||||
if (err && /Unknown index|Unknown collection/.test(err.message)) {
|
||||
this._hyperDbReady = false
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async upsert (row) {
|
||||
if (!row?.id || !row?.guildId) throw new Error('id and guildId required')
|
||||
const merged = { ...row, updatedAt: Date.now() }
|
||||
if (this.engine === 'hyperdb') {
|
||||
try {
|
||||
await this.db.insert(this.collection, merged)
|
||||
return merged
|
||||
} catch (err) {
|
||||
if (!this._disableHyperDb(err)) throw err
|
||||
}
|
||||
}
|
||||
await this._json.insert('@pearcord/scheduled-messages-json', merged)
|
||||
return merged
|
||||
}
|
||||
|
||||
async get (guildId, id) {
|
||||
if (!guildId || !id) return null
|
||||
if (this.engine === 'hyperdb') {
|
||||
try {
|
||||
return await this.db.get(this.collection, { guildId, id })
|
||||
} catch (err) {
|
||||
if (this._disableHyperDb(err)) {
|
||||
return this._json.get('@pearcord/scheduled-messages-json', { guildId, id })
|
||||
}
|
||||
if (/not found/i.test(err.message)) return null
|
||||
throw err
|
||||
}
|
||||
}
|
||||
return this._json.get('@pearcord/scheduled-messages-json', { guildId, id })
|
||||
}
|
||||
|
||||
async listByGuild (guildId, { channelId = null, status = 'queued', userId = null } = {}) {
|
||||
if (!guildId) return []
|
||||
let rows = []
|
||||
if (this.engine === 'hyperdb') {
|
||||
try {
|
||||
rows = await this.db.find(this.collection, { guildId })
|
||||
} catch (err) {
|
||||
if (this._disableHyperDb(err)) rows = []
|
||||
else throw err
|
||||
}
|
||||
} else {
|
||||
const raw = await this._json.find('@pearcord/scheduled-messages-json', { guildId })
|
||||
rows = Array.isArray(raw) ? raw : raw ? [raw] : []
|
||||
}
|
||||
return rows
|
||||
.filter((r) => !channelId || r.channelId === channelId)
|
||||
.filter((r) => !status || r.status === status)
|
||||
.filter((r) => !userId || r.userId === userId)
|
||||
.sort((a, b) => (a.sendAt || 0) - (b.sendAt || 0))
|
||||
}
|
||||
|
||||
async countQueued ({ guildId, channelId = null }) {
|
||||
const rows = await this.listByGuild(guildId, { channelId, status: 'queued' })
|
||||
return rows.length
|
||||
}
|
||||
|
||||
async assertQueueCapacity ({ guildId, channelId }) {
|
||||
const channelCount = await this.countQueued({ guildId, channelId })
|
||||
if (channelCount >= GUILD_SCHEDULE_MAX_QUEUED_PER_CHANNEL) {
|
||||
throw new Error(`scheduled message queue full (${GUILD_SCHEDULE_MAX_QUEUED_PER_CHANNEL} per channel)`)
|
||||
}
|
||||
const guildCount = await this.countQueued({ guildId })
|
||||
if (guildCount >= GUILD_SCHEDULE_MAX_QUEUED_PER_GUILD) {
|
||||
throw new Error(`scheduled message queue full (${GUILD_SCHEDULE_MAX_QUEUED_PER_GUILD} per guild)`)
|
||||
}
|
||||
}
|
||||
|
||||
async pruneStale (guildId) {
|
||||
const cutoff = Date.now() - DM_SCHEDULE_STALE_RETENTION_MS
|
||||
const rows = await this.listByGuild(guildId, { status: null })
|
||||
let pruned = 0
|
||||
for (const row of rows) {
|
||||
if (row.status === 'queued') continue
|
||||
const ts = row.sentAt || row.updatedAt || row.createdAt || 0
|
||||
if (ts >= cutoff) continue
|
||||
if (this.engine === 'hyperdb') {
|
||||
await this.db.delete(this.collection, { guildId: row.guildId, id: row.id }).catch(() => {})
|
||||
} else {
|
||||
await this._json.delete('@pearcord/scheduled-messages-json', { guildId: row.guildId, id: row.id })
|
||||
}
|
||||
pruned++
|
||||
}
|
||||
return { pruned }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ScheduledMessageStore,
|
||||
GUILD_SCHEDULE_MAX_QUEUED_PER_CHANNEL,
|
||||
GUILD_SCHEDULE_MAX_QUEUED_PER_GUILD
|
||||
}
|
||||
Generated
+62
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "pearcord-scheduled-messages",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pearcord-scheduled-messages",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"bare-path": "^3.0.0",
|
||||
"pearcord-db": "file:../pearcord-db",
|
||||
"pearcord-shared": "file:../pearcord-shared"
|
||||
}
|
||||
},
|
||||
"../pearcord-db": {
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"bare-fs": "^4.0.0",
|
||||
"bare-path": "^3.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"hyperdb": "^5.0.0",
|
||||
"hyperschema": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"../pearcord-shared": {
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"b4a": "^1.6.7",
|
||||
"compact-encoding": "^2.0.0",
|
||||
"hypercore-crypto": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-os": {
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz",
|
||||
"integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"bare": ">=1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-path": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.1.tgz",
|
||||
"integrity": "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-os": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pearcord-db": {
|
||||
"resolved": "../pearcord-db",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/pearcord-shared": {
|
||||
"resolved": "../pearcord-shared",
|
||||
"link": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "pearcord-scheduled-messages",
|
||||
"version": "0.1.0",
|
||||
"main": "index.js",
|
||||
"type": "commonjs",
|
||||
"description": "Guild + DM scheduled message queue for Pearcord",
|
||||
"dependencies": {
|
||||
"bare-path": "^3.0.0",
|
||||
"pearcord-db": "git+https://git.ssh.surf/pearcord/pearcord-db.git#main",
|
||||
"pearcord-shared": "git+https://git.ssh.surf/pearcord/pearcord-shared.git#main"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user