@hozu/cli 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -97,16 +97,44 @@ export default app({
97
97
  ```
98
98
  - The loop: change the declaration in TypeScript → `npx hozu gen` (writes the contract: types, the `Resolvers`
99
99
  interface, `Handler`) → implement the interface until `go test ./...` passes → restart the service → `hozu check`.
100
- A contract older than the declarations is HZ093; the service answers 409 to calls from other declarations.
100
+ A contract older than the declarations is HZ093 naming the changed effects; each effect has its own fingerprint, so
101
+ the service answers 409 only to calls of an effect that changed.
101
102
  - In Go: return a declared error as the error value (`hozu.NotesAddNoteDuplicate{Text: t}`), `hozu.Invalid{…}` for
102
103
  input problems; `ctx.Session` is nil when signed out; `ctx.SetSession(…)` / `ctx.SignOut()` in mutations. Public
103
104
  queries never receive the session. `ctx.File(token)` reads an upload, `ctx.Header` an endpoint's request headers
104
105
  (no cookie), `ctx.Preview` preview mode. The secret is required (16+ characters, the same value on both sides,
105
106
  HZ093): the service trusts the session it is sent, so serve `hozu.Handler(r, hozu.Options{Secret: …})` on a private
106
107
  address (it refuses to start without one).
108
+ - Any other Go error answers 500 with its first line, which reaches `onError` and `Unexpected` as a thrown TypeScript
109
+ error does, with the call's id (`x-hozu-call`) that the service's log line names
110
+ (`hozu: notes.listNotes (call 3fa2c1d0): …`). A service that is not running is `no service answers at <url>`.
107
111
  - `.meta({ title: 'Note' })` on a schema makes it one Go type wherever it appears; `z.int()` is `int64`, a plain
108
- number `float64`. Only `runs: 'server'` effects and JSON endpoints can be remote (HZ093).
109
- - `examples/notes-go` is the reference: the notes app with every resolver in Go.
112
+ number `float64` (`hozu gen` notes number fields named like ids or counts); a string `z.enum` is a named type with
113
+ one constant per member (`hozu.OrderStatusPending`, named by its title or its field). Only `runs: 'server'` effects
114
+ and JSON endpoints can be remote (HZ093).
115
+ - [`examples/notes-go`](https://github.com/olevatorr/Hozu/tree/main/examples/notes-go) is the reference: the notes
116
+ app with every resolver in Go. Its `service/main.go` is all a service needs besides the resolvers (`go.mod` is
117
+ yours; the contract's folder is the `hozu` package):
118
+ ```go
119
+ func main() {
120
+ secret := os.Getenv("NOTES_SERVICE_SECRET") // the app's remote() secret, 16+ characters
121
+ addr := os.Getenv("NOTES_SERVICE_ADDR") // the app's NOTES_SERVICE_URL is http://<addr>/effect
122
+ mux := http.NewServeMux()
123
+ mux.Handle("/effect", hozu.Handler(newResolvers(), hozu.Options{Secret: secret}))
124
+ server := &http.Server{Addr: addr, Handler: mux}
125
+ go func() {
126
+ if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
127
+ log.Fatal(err)
128
+ }
129
+ }()
130
+ stop, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
131
+ defer cancel()
132
+ <-stop.Done() // finish the calls in flight, then exit
133
+ ctx, done := context.WithTimeout(context.Background(), 5*time.Second)
134
+ defer done()
135
+ server.Shutdown(ctx)
136
+ }
137
+ ```
110
138
 
111
139
  ## A database
112
140
  - One pool per process, made in `app.ts`; close it in `app({ dispose: () => pool.end() })` so `hozu get`, `call` and
@@ -96,9 +96,10 @@ One machine per feature: an order list (filters, selection) and an order page (s
96
96
  `orders` and `order`, sharing declarations through `exports`. Each machine stays small and its contracts few.
97
97
 
98
98
  ## A multi-step checkout that also works without JavaScript
99
- Each step is a state; the server runs the machine per request, so without JS a step's form posts every earlier field
100
- again as hidden inputs (`ui.input({ type: 'hidden', name: 'line1', value: ctx.line1 })`), and the last step's
101
- mutation receives them all. Prefill from the member with `seed: ({ query }) => ({ email: query(me, {}).email })`.
99
+ Each step is a state and each step's form posts only its own fields: after a native post the server renders the next
100
+ step, and every form on that page carries the machine's state in a signed hidden field, so the next post continues
101
+ from it (going back to edit a step too). Prefill from the member with
102
+ `seed: ({ query }) => ({ email: query(me, {}).email })`.
102
103
 
103
104
  ## A notice after saving
104
105
  A `notice` context field set in `done` and cleared by `after: [{ ms: 4000, target: 'idle' }]` on a `saved` state;
@@ -69,6 +69,8 @@
69
69
  navigation prints `→ <path>` and the new page's lines; a live update on another actor's page prints under the step
70
70
  (`bob: + Milk`).
71
71
  - `≠ DIFFERS` marks a step where both modes made a request and the resulting text differs: a no-JS/JS parity bug.
72
+ It names the differing words (`≠ DIFFERS (on vs off): "#1307" vs "#1306"`): the two modes write twice to the same
73
+ data, so a new row per mode (an order number, a count) differs without a bug.
72
74
  - Errors: uncaught exceptions, `console.error`s, CSP violations and failed requests, each with the page, the
73
75
  resource type and the mode. A 400 re-render of an invalid native post is not an error, and a page answering
74
76
  401, 403, 404 or 410 is the step's status (`→ /notes/n1 (403)`), so an access check exits 0.
@@ -67,9 +67,11 @@ export const Board = ui.view({
67
67
  ## Menus, dialogs, counting
68
68
  - When a query's input changes, the rows stay (`aria-busy` on the parent) and update by key; `pending` shows only
69
69
  before the first answer.
70
- - A link to the page shown gets `aria-current="page"` (`"true"` for its section): `aria-[current]:font-bold`.
70
+ - A link to the page shown gets `aria-current="page"`, a link to a section above it (`/orders` on `/orders/7`)
71
+ `"true"`; the same path with another search (a next page) gets nothing. An `aria-current` you set wins.
71
72
  - `ui.dialog({ open: is(['editing']), on: { close: ui.send(Cancel, {}) } }, [...])` opens as a modal and closes with
72
- the machine; Escape sends `close`.
73
+ the machine; Escape sends `close`. It needs JavaScript: a dialog that must open without it uses the native
74
+ `commandfor` button (the short form) and closes when the data that shows it changes.
73
75
  - `ui.format.plural(n, { one: '# item', other: '# items' })` picks the case for the page's language (`=0` works).
74
76
  - `null` and `false` render nothing, also inside a constant list:
75
77
  `ui.ul({}, [...kinds.map((k) => (k === 'draft' ? null : ui.li({}, [k])))])`.