[{"data":1,"prerenderedAt":392},["ShallowReactive",2],{"fougere:post.list:{}":3,"tutorial-steps":6,"tutorial:\u002Ftutorial\u002F6-a-fact":43},{"items":4,"hasMore":5},[],false,[7,11,15,19,23,27,31,35,39],{"path":8,"step":9,"name":10},"\u002Ftutorial\u002F0-start",0,"Start",{"path":12,"step":13,"name":14},"\u002Ftutorial\u002F1-an-entity",1,"An entity",{"path":16,"step":17,"name":18},"\u002Ftutorial\u002F2-a-source",2,"A source",{"path":20,"step":21,"name":22},"\u002Ftutorial\u002F3-a-rule",3,"A rule",{"path":24,"step":25,"name":26},"\u002Ftutorial\u002F4-a-port",4,"A port",{"path":28,"step":29,"name":30},"\u002Ftutorial\u002F5-a-link",5,"A link",{"path":32,"step":33,"name":34},"\u002Ftutorial\u002F6-a-fact",6,"A fact",{"path":36,"step":37,"name":38},"\u002Ftutorial\u002F7-an-app",7,"An app",{"path":40,"step":41,"name":42},"\u002Ftutorial\u002F8-split",8,"Split",{"id":44,"title":45,"aside":46,"body":47,"code":53,"concept":348,"description":349,"extension":350,"fig":351,"files":352,"grows":362,"heading":375,"install":46,"lede":349,"links":376,"meta":383,"name":34,"navigation":96,"path":32,"seo":384,"slug":385,"stem":386,"step":33,"tryIt":387,"zero":46,"__hash__":391},"tutorial\u002Ftutorial\u002F6-a-fact.md","Mail is its own domain",null,{"type":48,"value":49,"toc":50},"minimark",[],{"title":51,"searchDepth":17,"depth":17,"links":52},"",[],[54,77,90,147,181,216,236,247,276],{"path":55,"state":51,"lines":56},"fronds\u002Fblog\u002Fentities\u002FPost.ts",[57,60,62,65,68,71,74],{"text":58,"added":5,"keys":59},"import { entity, oneOf, primary, readOnly, text } from '@fougere\u002Fschema';",[],{"text":51,"added":5,"keys":61},[],{"text":63,"added":5,"keys":64},"export default class Post extends entity({",[],{"text":66,"added":5,"keys":67},"  id: primary(),",[],{"text":69,"added":5,"keys":70},"  title: text({ min: 1, max: 160 }),",[],{"text":72,"added":5,"keys":73},"  status: readOnly(oneOf('draft', 'published', { default: 'draft' })),",[],{"text":75,"added":5,"keys":76},"}) {}",[],{"path":78,"state":79,"lines":80},"fronds\u002Fblog\u002Fentities\u002FPostPublished.ts","new",[81,84,86],{"text":82,"added":5,"keys":83},"import Post from '.\u002FPost.js';",[],{"text":51,"added":5,"keys":85},[],{"text":87,"added":5,"keys":88},"export default class PostPublished extends Post.pick('id', 'title') {}",[89],"c",{"path":91,"state":92,"lines":93},"fronds\u002Fblog\u002Fhandlers\u002FPostHandler.ts","changed",[94,98,101,104,106,109,112,115,118,120,123,126,129,132,134,137,139,142,144],{"text":95,"added":96,"keys":97},"import { ErrorCode, FougereError, Repository, type Emit } from '@fougere\u002Fcore';",true,[],{"text":99,"added":5,"keys":100},"import Post from '..\u002Fentities\u002FPost.js';",[],{"text":102,"added":96,"keys":103},"import type PostPublished from '..\u002Fentities\u002FPostPublished.js';",[],{"text":51,"added":5,"keys":105},[],{"text":107,"added":5,"keys":108},"export default class PostHandler extends Repository\u003CPost> {",[],{"text":110,"added":96,"keys":111},"  constructor(private published: Emit\u003CPostPublished>) {",[],{"text":113,"added":5,"keys":114},"    super();",[],{"text":116,"added":5,"keys":117},"  }",[],{"text":51,"added":5,"keys":119},[],{"text":121,"added":5,"keys":122},"  async publish(id: Post['id']): Promise\u003CPost> {",[],{"text":124,"added":5,"keys":125},"    const post = await this.findById(id);",[],{"text":127,"added":5,"keys":128},"    if (!post) throw new FougereError({ code: ErrorCode.NOT_FOUND, message: 'No such post' });",[],{"text":130,"added":5,"keys":131},"    if (post.status === 'published') throw new FougereError({ code: ErrorCode.CONFLICT, message: 'Already published' });",[],{"text":51,"added":5,"keys":133},[],{"text":135,"added":96,"keys":136},"    await this.published({ id: post.id, title: post.title });",[],{"text":51,"added":5,"keys":138},[],{"text":140,"added":5,"keys":141},"    return this.update(id, { status: 'published' });",[],{"text":116,"added":5,"keys":143},[],{"text":145,"added":5,"keys":146},"}",[],{"path":148,"state":79,"lines":149},"fronds\u002Fmail\u002Fhandlers\u002FMailHandler.ts",[150,153,156,159,161,164,167,169,174,177,179],{"text":151,"added":5,"keys":152},"import type { Fact } from '@fougere\u002Fcore';",[],{"text":154,"added":5,"keys":155},"import type PostPublished from '@fronds\u002Fblog\u002Fentities\u002FPostPublished.js';",[],{"text":157,"added":5,"keys":158},"import Mailer from '..\u002Fservices\u002FMailer.js';",[],{"text":51,"added":5,"keys":160},[],{"text":162,"added":5,"keys":163},"export default class MailHandler {",[],{"text":165,"added":5,"keys":166},"  constructor(private mailer: Mailer) {}",[],{"text":51,"added":5,"keys":168},[],{"text":170,"added":5,"keys":171},"  async onPublished(post: Fact\u003CPostPublished>): Promise\u003Cvoid> {",[172,173],"a","b",{"text":175,"added":5,"keys":176},"    await this.mailer.send(`Published: ${post.title}`);",[],{"text":116,"added":5,"keys":178},[],{"text":145,"added":5,"keys":180},[],{"path":182,"state":183,"lines":184},"fronds\u002Fmail\u002Fservices\u002FAuditMailer.ts","moved",[185,188,191,193,196,199,201,204,207,209,212,214],{"text":186,"added":5,"keys":187},"import type { Logger } from '@fougere\u002Fcore';",[],{"text":189,"added":5,"keys":190},"import type Mailer from '.\u002FMailer.js';",[],{"text":51,"added":5,"keys":192},[],{"text":194,"added":5,"keys":195},"export default class AuditMailer implements Partial\u003CMailer> {",[],{"text":197,"added":5,"keys":198},"  constructor(private inner: Mailer, private log: Logger) {}",[],{"text":51,"added":5,"keys":200},[],{"text":202,"added":5,"keys":203},"  async send(subject: string): Promise\u003Cvoid> {",[],{"text":205,"added":5,"keys":206},"    this.log.info('sending', subject);",[],{"text":51,"added":5,"keys":208},[],{"text":210,"added":5,"keys":211},"    return this.inner.send(subject);",[],{"text":116,"added":5,"keys":213},[],{"text":145,"added":5,"keys":215},[],{"path":217,"state":183,"lines":218},"fronds\u002Fmail\u002Fservices\u002FConsoleMailer.ts",[219,222,224,227,229,232,234],{"text":220,"added":5,"keys":221},"import Mailer from '.\u002FMailer.js';",[],{"text":51,"added":5,"keys":223},[],{"text":225,"added":5,"keys":226},"export default class ConsoleMailer extends Mailer {",[],{"text":202,"added":5,"keys":228},[],{"text":230,"added":5,"keys":231},"    console.log(`mail: ${subject}`);",[],{"text":116,"added":5,"keys":233},[],{"text":145,"added":5,"keys":235},[],{"path":237,"state":183,"lines":238},"fronds\u002Fmail\u002Fservices\u002FMailer.ts",[239,242,245],{"text":240,"added":5,"keys":241},"export default abstract class Mailer {",[],{"text":243,"added":5,"keys":244},"  abstract send(subject: string): Promise\u003Cvoid>;",[],{"text":145,"added":5,"keys":246},[],{"path":248,"state":51,"lines":249},"fougere.config.ts",[250,253,256,259,261,264,267,270,273],{"text":251,"added":5,"keys":252},"import '@fougere\u002Fadapter-rest';",[],{"text":254,"added":5,"keys":255},"import '@fougere\u002Fadapter-sql';",[],{"text":257,"added":5,"keys":258},"import { defineFougere } from '@fougere\u002Fcore';",[],{"text":51,"added":5,"keys":260},[],{"text":262,"added":5,"keys":263},"export default defineFougere({",[],{"text":265,"added":5,"keys":266},"  protocols: { rest: {} },",[],{"text":268,"added":5,"keys":269},"  sources: { main: { type: 'sqlite', path: '.data\u002Fblog.db' } },",[],{"text":271,"added":5,"keys":272},"  fronds: { '@fougere\u002Flog': {} },",[],{"text":274,"added":5,"keys":275},"});",[],{"path":277,"state":51,"lines":278},"package.json",[279,282,285,288,291,294,297,300,303,306,309,312,315,318,321,324,326,329,332,335,338,341,344,346],{"text":280,"added":5,"keys":281},"{",[],{"text":283,"added":5,"keys":284},"  \"name\": \"my-blog\",",[],{"text":286,"added":5,"keys":287},"  \"private\": true,",[],{"text":289,"added":5,"keys":290},"  \"type\": \"module\",",[],{"text":292,"added":5,"keys":293},"  \"scripts\": {",[],{"text":295,"added":5,"keys":296},"    \"typecheck\": \"tsc\",",[],{"text":298,"added":5,"keys":299},"    \"test\": \"vitest run --config tests\u002Fvitest.config.ts\"",[],{"text":301,"added":5,"keys":302},"  },",[],{"text":304,"added":5,"keys":305},"  \"dependencies\": {",[],{"text":307,"added":5,"keys":308},"    \"@fougere\u002Fadapter-rest\": \"latest\",",[],{"text":310,"added":5,"keys":311},"    \"@fougere\u002Fadapter-sql\": \"latest\",",[],{"text":313,"added":5,"keys":314},"    \"@fougere\u002Fcore\": \"latest\",",[],{"text":316,"added":5,"keys":317},"    \"@fougere\u002Flog\": \"latest\",",[],{"text":319,"added":5,"keys":320},"    \"@fougere\u002Fschema\": \"latest\",",[],{"text":322,"added":5,"keys":323},"    \"better-sqlite3\": \"^13.0.3\"",[],{"text":301,"added":5,"keys":325},[],{"text":327,"added":5,"keys":328},"  \"devDependencies\": {",[],{"text":330,"added":5,"keys":331},"    \"@fougere\u002Fcli\": \"latest\",",[],{"text":333,"added":5,"keys":334},"    \"@fougere\u002Ftesting\": \"latest\",",[],{"text":336,"added":5,"keys":337},"    \"@types\u002Fnode\": \"^22.19.15\",",[],{"text":339,"added":5,"keys":340},"    \"typescript\": \"^7.0.2\",",[],{"text":342,"added":5,"keys":343},"    \"vitest\": \"^4.1.0\"",[],{"text":116,"added":5,"keys":345},[],{"text":145,"added":5,"keys":347},[],"fact","The blog announces that a post was published, and a second frond receives it. The mailer and its log move into that frond.","md","The blog names nobody. Accepting a Fact\u003CT> is the subscription.",[353,354,355,356,357,358,359,360,361],{"path":55,"state":51},{"path":78,"state":79},{"path":91,"state":92},{"path":148,"state":79},{"path":182,"state":183},{"path":217,"state":183},{"path":237,"state":183},{"path":248,"state":51},{"path":277,"state":51},[363,367,371],{"key":172,"side":364,"label":365,"detail":366},"in","no route for onPublished","a listener has no caller",{"key":173,"side":368,"label":369,"detail":370},"out","blog → mail","an edge in fougere graph",{"key":89,"side":372,"label":373,"detail":374},"refuses","Unknown field","a fact is judged on arrival","Mail is *its own domain*",[377,380],{"title":378,"path":379},"Fact","\u002Fdocs\u002Fconcepts\u002Ffact",{"title":381,"path":382},"Facade","\u002Fdocs\u002Fconcepts\u002Ffacade",{},{"title":45,"description":349},"6-a-fact","tutorial\u002F6-a-fact",{"command":388,"said":389,"output":390},"curl -X POST localhost:4100\u002Fapi\u002Fblog\u002Fposts\u002F$ID\u002Fpublish",{"npm":388,"pnpm":388,"bun":388,"yarn":388},"INF [app:mail:AuditMailer] (mail:MailHandler.onPublished) sending Published: Hi\nmail: Published: Hi","BGWkvq1yS3s9bEKwJOG6XZbSNn-4poZWsR6vdV_EheE",1794396615751]