import { Note, NoteBoard, CreateNoteRequest, SignInRequest, Session } from "../types/notes.nox"
// The board is a `live` resource: the compiler publishes an invalidate-only
// notice over one multiplexed SSE controller after any validated mutation,
// and the acquisition re-runs the declared GET.
resource Board(boardId: String): NoteBoard {
get {
GET "/api/notes/{boardId}"
}
live
cache 30s
reload stale_while_revalidate
retry 2
}
component AppPage {
route {
title: "Noxid App"
description: "A typed form, a typed endpoint, and a live resource"
render: client
}
state {
displayName: String = ""
signedInAs: String = ""
sessionMessage: String = "Sign in to open your board."
draftTitle: String = ""
formMessage: String = "Type a note title, then submit it."
createdNotes: Array<Note> = []
}
resources {
board = Board(boardId: "welcome")
}
computed {
pendingCount = createdNotes.countWhere(done, false)
}
actions {
// The sign-in door. server/middleware/session.ts mints the signed
// session cookie for the validated display name; this action reports
// the principal the runtime bound. It is a starter placeholder —
// replace it with a real identity provider.
server signIn(request: SignInRequest): Session invalidates [Board] {
}
server signOut(): Boolean {
}
server createNote(request: CreateNoteRequest): Note {
}
validateDraft() {
if len(trim(draftTitle)) == 0 {
formMessage = "A note title is required."
} else {
formMessage = "Draft is valid and ready to submit."
}
}
submitSignIn() {
let request = SignInRequest(displayName = trim(displayName))
sessionMessage = "Signing in…"
let outcome = await signIn(request: request)
#match outcome {
Ok(session) {
signedInAs = session.userId
displayName = ""
sessionMessage = `Signed in as {session.userId}.`
}
Err(error) {
sessionMessage = `Sign-in failed: {error.message}`
}
}
}
submitSignOut() {
let outcome = await signOut()
#match outcome {
Ok(cleared) {
signedInAs = ""
createdNotes = []
sessionMessage = "Signed out. Your notes stay with your principal."
}
Err(error) {
sessionMessage = `Sign-out failed: {error.message}`
}
}
}
// The view only offers this once `signedInAs` is set, and the server
// refuses it outright without a session: the `Err` arm is where the
// structured SESSION_PRINCIPAL_REQUIRED refusal becomes something the
// person reading the page can act on.
submit() {
let request = CreateNoteRequest(boardId = "welcome", title = trim(draftTitle))
formMessage = "Saving…"
let outcome = await createNote(request: request)
#match outcome {
Ok(note) {
createdNotes.push(note)
draftTitle = ""
formMessage = "Saved."
}
Err(error) {
formMessage = `Save failed: {error.message}`
}
}
}
}
intent {
purpose: "Let one signed-in person add notes to a board and see the board stay current"
success: "A submitted note is persisted through the typed boundary and appears in the list"
constraints: ["Never show a note the server has not validated", "Never write without a session"]
}
invariant SavedNotesAreValidated {
assert: createdNotes.countWhere(id, "") == 0
depends: [createdNotes]
}
requirement APP_SIGN_IN {
description: "Signing in binds the page to one principal and opens the board"
verify: ["run the executable scenario"]
depends: [displayName, signedInAs, submitSignIn, signIn]
}
requirement APP_SESSION_REQUIRED {
description: "A submit with no session is refused, stores nothing, and shows the refusal"
verify: ["run the executable scenario"]
depends: [createdNotes, formMessage, submit, createNote]
}
requirement APP_NOTE_CREATE {
description: "Submitting a non-empty draft stores the validated note and clears the draft"
verify: ["run the executable scenario"]
depends: [draftTitle, createdNotes, submit, createNote]
}
scenario SignInOpensTheBoard {
description: "signing in binds the page to the principal the server returned"
given: displayName = "ada", signedInAs = "", board = Ready(NoteBoard(boardId = "welcome", notes = [])), signIn = Ok(Session(userId = "ada"))
when: submitSignIn()
expect: signedInAs == "ada", displayName == "", sessionMessage == "Signed in as ada."
covers: [APP_SIGN_IN]
}
scenario RefusesWriteWithoutSession {
description: "the server's signed-out refusal stores nothing and reaches the page"
given: signedInAs = "", draftTitle = "Write the launch note", createdNotes = [], board = Ready(NoteBoard(boardId = "welcome", notes = [])), createNote = Err(RemoteError(code = "SESSION_PRINCIPAL_REQUIRED", message = "this request carries no session"))
when: submit()
expect: createdNotes == [], draftTitle == "Write the launch note", formMessage == "Save failed: this request carries no session"
covers: [APP_SESSION_REQUIRED]
}
scenario CreateNote {
description: "a valid draft becomes a validated note under the signed-in principal"
given: signedInAs = "ada", draftTitle = "Write the launch note", createdNotes = [], board = Ready(NoteBoard(boardId = "welcome", notes = [])), createNote = Ok(Note(id = "note-1", title = "Write the launch note", done = false))
when: submit()
expect: createdNotes == [Note(id = "note-1", title = "Write the launch note", done = false)], draftTitle == "", formMessage == "Saved."
covers: [APP_NOTE_CREATE]
}
view {
<main>
<h1>Noxid App</h1>
#if len(signedInAs) == 0 {
<section class="card">
<h2>Sign in</h2>
<p>Every endpoint and action here needs a session. This form is a starter placeholder: it signs a cookie for the name you type. Replace it with your identity provider.</p>
<label for="display-name">Display name</label>
<input id="display-name" value:bind={displayName} />
<p>{sessionMessage}</p>
<button +click={submitSignIn}>Sign in</button>
</section>
} #else {
<section class="card">
<h2>Session</h2>
<p>Signed in as {signedInAs}. {sessionMessage}</p>
<button +click={submitSignOut}>Sign out</button>
</section>
<section class="card">
<h2>Board</h2>
#match board {
Idle {
<p>Waiting to acquire the board.</p>
}
Loading {
<p>Loading the board…</p>
}
Ready(data) {
<div>
<p>Board {data.boardId} · {data.notes.count()} notes.</p>
#for note in data.notes key note.id {
<article class="note">{note.title}</article>
}
</div>
}
Refreshing(data) {
<div>
<p>Refreshing board {data.boardId}…</p>
#for note in data.notes key note.id {
<article class="note">{note.title}</article>
}
</div>
}
Failed(error) {
<p class="error">{error.code}: {error.message}</p>
}
}
</section>
<section class="card">
<h2>Add a note</h2>
<label for="note-title">Title</label>
<input id="note-title" value:bind={draftTitle} />
<p>{formMessage}</p>
<button +click={validateDraft}>Validate draft</button>
<button +click={submit}>Save note</button>
<p>{pendingCount} unfinished note(s) saved this session.</p>
#for note in createdNotes key note.id {
<article class="note">{note.title}</article>
}
</section>
}
</main>
}
style {
main { display: grid; gap: 1rem; padding: 2rem; }
.card { padding: 1rem; border: 1px solid #d7dee8; border-radius: 0.75rem; }
.note { padding: 0.5rem 0; border-top: 1px solid #e4e9ef; }
input { width: 100%; box-sizing: border-box; padding: 0.6rem; font: inherit; }
button { padding: 0.6rem 0.9rem; border: 0; border-radius: 0.5rem; background: #154f8b; color: #ffffff; cursor: pointer; }
.error { color: #a32121; }
}
}