@trypattern/sdk 0.1.0 → 0.1.1

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.
Files changed (92) hide show
  1. package/dist/api.d.ts +17 -7
  2. package/dist/api.js +34 -8
  3. package/dist/app.d.ts +12 -3
  4. package/dist/app.js +49 -7
  5. package/dist/appInfo.d.ts +35 -0
  6. package/dist/appInfo.js +67 -0
  7. package/dist/appTitle.d.ts +6 -0
  8. package/dist/appTitle.js +3 -0
  9. package/dist/builtins/AppSettings.d.ts +12 -0
  10. package/dist/builtins/AppSettings.js +31 -0
  11. package/dist/builtins/Toasts.d.ts +1 -1
  12. package/dist/builtins/Toasts.js +2 -1
  13. package/dist/cli/app.js +91 -14
  14. package/dist/cli/publishing.d.ts +21 -0
  15. package/dist/cli/publishing.js +493 -0
  16. package/dist/clock.d.ts +6 -0
  17. package/dist/clock.js +31 -0
  18. package/dist/components/AppFrame.js +3 -3
  19. package/dist/components/Button.d.ts +2 -1
  20. package/dist/components/Button.js +6 -3
  21. package/dist/components/CopyButton.d.ts +11 -0
  22. package/dist/components/CopyButton.js +16 -0
  23. package/dist/components/Divider.d.ts +2 -1
  24. package/dist/components/Divider.js +19 -4
  25. package/dist/components/IconButton.js +2 -2
  26. package/dist/components/Link.d.ts +8 -3
  27. package/dist/components/Link.js +11 -2
  28. package/dist/components/ListRow.js +2 -2
  29. package/dist/components/Menu.js +2 -2
  30. package/dist/components/RadioGroup.d.ts +18 -0
  31. package/dist/components/RadioGroup.js +85 -0
  32. package/dist/components/SegmentedControl.js +2 -2
  33. package/dist/components/Sidebar.js +2 -3
  34. package/dist/components/SignInScreen.js +3 -3
  35. package/dist/components/Switch.js +2 -1
  36. package/dist/components/controlLook.d.ts +10 -0
  37. package/dist/components/controlLook.js +14 -1
  38. package/dist/components/icons.d.ts +4 -3
  39. package/dist/components/icons.js +9 -3
  40. package/dist/components/interaction.d.ts +1 -1
  41. package/dist/flows/accessFlows.js +19 -3
  42. package/dist/flows/browser.d.ts +3 -1
  43. package/dist/flows/browser.js +6 -3
  44. package/dist/flows/flowBrowser.d.ts +3 -1
  45. package/dist/flows/flowBrowser.js +4 -2
  46. package/dist/flows/flows.d.ts +6 -0
  47. package/dist/flows/flows.js +32 -5
  48. package/dist/hooks/appContext.d.ts +2 -1
  49. package/dist/index.d.ts +6 -2
  50. package/dist/index.js +4 -2
  51. package/dist/publishWire.d.ts +271 -0
  52. package/dist/publishWire.js +168 -0
  53. package/dist/react.d.ts +3 -0
  54. package/dist/react.js +2 -0
  55. package/dist/registered.d.ts +6 -0
  56. package/dist/registered.js +2 -0
  57. package/dist/rules/checkAllFiles.js +10 -2
  58. package/dist/rules/lintPlugin.js +2 -0
  59. package/dist/rules/messages.d.ts +2 -2
  60. package/dist/rules/messages.js +2 -2
  61. package/dist/schema.d.ts +2 -0
  62. package/dist/schema.js +1 -0
  63. package/dist/server/auth.js +4 -1
  64. package/dist/server/createServer.d.ts +48 -3
  65. package/dist/server/createServer.js +25 -5
  66. package/dist/server/env.d.ts +2 -2
  67. package/dist/server/index.d.ts +6 -1
  68. package/dist/server/index.js +5 -1
  69. package/dist/server/services.d.ts +9 -0
  70. package/dist/server/services.js +47 -0
  71. package/dist/server/session.d.ts +1 -0
  72. package/dist/server/session.js +11 -0
  73. package/dist/server/settings.d.ts +49 -0
  74. package/dist/server/settings.js +25 -0
  75. package/dist/server/signInWire.d.ts +1 -0
  76. package/dist/server/signInWire.js +2 -1
  77. package/dist/serviceClients.d.ts +5 -0
  78. package/dist/serviceClients.js +70 -0
  79. package/dist/services.d.ts +53 -0
  80. package/dist/services.js +30 -0
  81. package/dist/start.js +30 -6
  82. package/dist/store.d.ts +5 -3
  83. package/dist/store.js +13 -3
  84. package/dist/table.d.ts +1 -1
  85. package/dist/vite/localWorker.d.ts +1 -1
  86. package/dist/vite/localWorker.js +5 -4
  87. package/dist/vite/projectFile.d.ts +9 -0
  88. package/dist/vite/projectFile.js +17 -0
  89. package/dist/vite/vite.d.ts +2 -1
  90. package/dist/vite/vite.js +10 -3
  91. package/package.json +5 -1
  92. package/presets/register.d.ts +2 -0
package/dist/api.d.ts CHANGED
@@ -1,20 +1,30 @@
1
1
  import type { SessionApi, SessionCall } from './auth/sessionApi.ts';
2
+ import type { AppInfoApi, AppInfoCall } from './appInfo.ts';
2
3
  import type { Gate } from './gate.ts';
4
+ import type { Services } from './services.ts';
3
5
  import type { Seed, Tables } from './table.ts';
4
6
  import type { TableCall, TablesApi } from './tables.ts';
5
7
  export type AppApi<Group extends Tables> = {
6
8
  tables: TablesApi<Group>;
7
9
  session: SessionApi;
10
+ app: AppInfoApi;
11
+ services: object;
8
12
  };
9
- export type ApiCall<Group extends Tables> = TableCall<Group> | `session.${SessionCall}`;
10
- export declare function apiCalls<Group extends Tables>(tables: Group): readonly ApiCall<Group>[];
11
- export declare function httpApi<Group extends Tables>(tables: Group, gate: Gate): AppApi<Group>;
12
- export declare function memoryApi<Group extends Tables>(tables: Group, seed: Seed<Group>, { gate, signedInAs, admins }: {
13
+ export type ServicesSetup = {
14
+ services?: Services;
15
+ memory?: object;
16
+ world?: unknown;
17
+ };
18
+ export type ApiCall<Group extends Tables> = TableCall<Group> | `session.${SessionCall}` | `app.${AppInfoCall}`;
19
+ export declare function apiCalls<Group extends Tables>(tables: Group, services?: Services): readonly ApiCall<Group>[];
20
+ export declare function httpApi<Group extends Tables>(tables: Group, gate: Gate, setup?: ServicesSetup): AppApi<Group>;
21
+ export declare function memoryApi<Group extends Tables>(tables: Group, seed: Seed<Group>, { gate, signedInAs, admins, title, ...setup }: {
13
22
  gate: Gate;
14
23
  signedInAs: string | null;
15
24
  admins: readonly string[];
16
- }): AppApi<Group>;
17
- export declare function browserApi<Group extends Tables>(tables: Group, seed: Seed<Group>, gate: Gate, { inMemory, admins }: {
25
+ title: string;
26
+ } & ServicesSetup): AppApi<Group>;
27
+ export declare function browserApi<Group extends Tables>(tables: Group, seed: Seed<Group>, gate: Gate, { inMemory, admins, ...setup }: {
18
28
  inMemory: boolean;
19
29
  admins: readonly string[];
20
- }): AppApi<Group>;
30
+ } & ServicesSetup): AppApi<Group>;
package/dist/api.js CHANGED
@@ -1,34 +1,60 @@
1
1
  import { adminEmails, viewerByEmail, viewerOf } from "./access.js";
2
2
  import { HttpSessionApi, MemorySessionApi, sessionCalls } from "./auth/sessionApi.js";
3
+ import { HttpAppInfoApi, MemoryAppInfoApi, appInfoCalls } from "./appInfo.js";
4
+ import { httpServices, memoryServices } from "./serviceClients.js";
5
+ import { serviceCallNames } from "./services.js";
3
6
  import { httpTables, memoryTables, tableCalls } from "./tables.js";
4
- export function apiCalls(tables) {
7
+ export function apiCalls(tables, services = {}) {
5
8
  return [
9
+ ...serviceCallNames(services),
6
10
  ...tableCalls(tables),
7
- ...sessionCalls.map((call) => `session.${call}`)
11
+ ...sessionCalls.map((call) => `session.${call}`),
12
+ ...appInfoCalls.map((call) => `app.${call}`)
8
13
  ];
9
14
  }
10
- export function httpApi(tables, gate) {
15
+ function memoryClient(setup, options) {
16
+ if (setup.services === undefined)
17
+ return {};
18
+ if (setup.memory === undefined) {
19
+ throw new Error('This app declares services but no memory fake: pass defineApp({ services, memory }) so flows and ?memory can run without a server.');
20
+ }
21
+ return memoryServices(setup.services, setup.memory, setup.world ?? {}, options);
22
+ }
23
+ export function httpApi(tables, gate, setup = {}) {
11
24
  const session = new HttpSessionApi(gate);
12
25
  return {
13
26
  tables: httpTables(tables, gate, () => session.reportSignedOut()),
14
- session
27
+ session,
28
+ app: new HttpAppInfoApi(gate),
29
+ services: setup.services === undefined
30
+ ? {}
31
+ : httpServices(setup.services, gate, () => session.reportSignedOut())
15
32
  };
16
33
  }
17
- export function memoryApi(tables, seed, { gate, signedInAs, admins }) {
34
+ export function memoryApi(tables, seed, { gate, signedInAs, admins, title, ...setup }) {
18
35
  const options = { latencyMs: 0, gate };
19
36
  const session = new MemorySessionApi(signedInAs, admins, options);
20
37
  return {
21
38
  tables: memoryTables(tables, seed, () => viewerOf(session.rememberedUser()), options),
22
- session
39
+ session,
40
+ app: new MemoryAppInfoApi(title, options),
41
+ services: memoryClient(setup, options)
23
42
  };
24
43
  }
25
- export function browserApi(tables, seed, gate, { inMemory, admins }) {
44
+ export function browserApi(tables, seed, gate, { inMemory, admins, ...setup }) {
26
45
  const session = new HttpSessionApi(gate);
27
46
  const emails = adminEmails(admins);
47
+ const options = { latencyMs: 0, gate };
28
48
  return {
29
49
  tables: inMemory
30
50
  ? memoryTables(tables, seed, () => viewerByEmail(session.rememberedUser(), emails), { latencyMs: 0, gate })
31
51
  : httpTables(tables, gate, () => session.reportSignedOut()),
32
- session
52
+ session,
53
+ app: new HttpAppInfoApi(gate),
54
+ services: inMemory
55
+ ? memoryClient(setup, options)
56
+ : setup.services === undefined
57
+ ? {}
58
+ : httpServices(setup.services, gate, () => session.reportSignedOut())
33
59
  };
34
60
  }
package/dist/app.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import type { DataRouter } from 'react-router';
2
2
  import type { AppApi } from './api.ts';
3
+ import { AppStore } from './appInfo.ts';
3
4
  import { SessionStore } from './auth/sessionStore.ts';
4
5
  import type { Cache } from './cache.ts';
6
+ import type { Clock } from './clock.ts';
5
7
  import type { Gate } from './gate.ts';
6
8
  import { ActionsStore } from './actions.ts';
7
9
  import type { Action, ActionsBuilder } from './actions.ts';
@@ -12,6 +14,7 @@ import type { Snapshots } from './store.ts';
12
14
  import type { Json, Persisted } from './state.ts';
13
15
  import type { DerivedOf, Instances, MessagesOf, ModelClassLike, ModelSnapshots } from './model.ts';
14
16
  import type { PageOf, PathOf, RouteDefinition } from './routes.ts';
17
+ import type { Services } from './services.ts';
15
18
  import type { Tables } from './table.ts';
16
19
  import { PublicTableStore, TableStore } from './tables.ts';
17
20
  type Observed = {
@@ -21,6 +24,7 @@ type Observed = {
21
24
  export type { DataRouter };
22
25
  export type ModelServicesOf<Path extends string> = {
23
26
  navigate: (to: Path) => Promise<void>;
27
+ every: (ms: number, run: () => void | Promise<void>) => () => void;
24
28
  };
25
29
  type StaticTypeError<Text extends string> = Text;
26
30
  type NonJsonGetters<Instance> = {
@@ -39,6 +43,7 @@ export type AppDependencies<Api> = {
39
43
  api: Api;
40
44
  cache: Cache;
41
45
  gate: Gate;
46
+ clock?: Clock;
42
47
  };
43
48
  export type RunningApp<Command, Snapshot extends {
44
49
  readonly path: string;
@@ -50,8 +55,9 @@ export type RunningApp<Command, Snapshot extends {
50
55
  };
51
56
  getSnapshot(): Snapshot;
52
57
  whenIdle(): Promise<void>;
58
+ tick(): void;
53
59
  };
54
- export type StoreClasses = Record<string, new (persisted: Persisted) => Observed>;
60
+ export type StoreClasses = Record<string, new (persisted: Persisted, services: never) => Observed>;
55
61
  export type StoreInstances<Classes extends StoreClasses> = {
56
62
  [Key in keyof Classes]: InstanceType<Classes[Key]>;
57
63
  };
@@ -65,12 +71,12 @@ export type PublicStores<Group extends Tables> = {
65
71
  };
66
72
  export type StoresOf<Group extends Tables, Custom, Item extends Action = Action> = {
67
73
  session: SessionStore;
74
+ app: AppStore;
68
75
  actions: ActionsStore<Item>;
69
76
  toasts: Toasts;
70
77
  page: PageStore;
71
78
  } & TableStores<Group> & PublicStores<Group> & Custom;
72
79
  export type AppDefinition<Group extends Tables, Custom extends StoreClasses, Classes extends Record<string, ModelClassLike>, Routes extends readonly RouteDefinition[]> = {
73
- name: string;
74
80
  tables: Group;
75
81
  stores?: Custom;
76
82
  models: {
@@ -79,12 +85,16 @@ export type AppDefinition<Group extends Tables, Custom extends StoreClasses, Cla
79
85
  routes: Routes;
80
86
  actions?: readonly ActionsBuilder<never, Action>[];
81
87
  actionGroups?: readonly string[];
88
+ services?: Services;
89
+ memory?: object;
82
90
  };
83
91
  export type AppClass<Group extends Tables, Instance> = {
84
92
  new (router: DataRouter, deps: AppDependencies<AppApi<Group>>): Instance;
85
93
  readonly definition: {
86
94
  readonly tables: Group;
87
95
  readonly routes: readonly RouteDefinition[];
96
+ readonly services?: Services;
97
+ readonly memory?: object;
88
98
  };
89
99
  };
90
100
  export type AppSnapshotShape<Models, Stores, Page> = {
@@ -96,7 +106,6 @@ export type AppSnapshotShape<Models, Stores, Page> = {
96
106
  type AppCommandOf<Classes extends Record<string, ModelClassLike>, Routes extends readonly RouteDefinition[]> = MessagesOf<Instances<BuiltInModels>> | MessagesOf<Instances<Classes>> | NavigateCommand<PathOf<Routes>> | PressKeyCommand;
97
107
  export type DefinedApp<Group extends Tables, Classes extends Record<string, ModelClassLike>, Routes extends readonly RouteDefinition[], Custom extends StoreClasses> = {
98
108
  new (router: DataRouter, dependencies: AppDependencies<AppApi<Group>>): RunningApp<AppCommandOf<Classes, Routes>, AppSnapshotShape<Instances<BuiltInModels & Classes>, StoresOf<Group, StoreInstances<Custom>>, PageOf<Routes>>> & {
99
- readonly name: string;
100
109
  readonly models: Instances<BuiltInModels & Classes>;
101
110
  readonly stores: StoresOf<Group, StoreInstances<Custom>>;
102
111
  pressKey(key: string): Promise<void> | undefined;
package/dist/app.js CHANGED
@@ -1,8 +1,11 @@
1
1
  import { z } from 'zod';
2
2
  import { viewerOf } from "./access.js";
3
+ import { AppStore } from "./appInfo.js";
3
4
  import { SessionStore } from "./auth/sessionStore.js";
5
+ import { realClock } from "./clock.js";
4
6
  import { ActionsStore } from "./actions.js";
5
7
  import { Account } from "./builtins/Account.js";
8
+ import { AppSettings } from "./builtins/AppSettings.js";
6
9
  import { BUILT_IN_GROUPS, builtInActions } from "./builtins/actions.js";
7
10
  import { Palette } from "./builtins/Palette.js";
8
11
  import { SignIn } from "./builtins/SignIn.js";
@@ -43,7 +46,12 @@ function clashOf(groups) {
43
46
  }
44
47
  return undefined;
45
48
  }
46
- const builtInModels = { account: Account, palette: Palette, signIn: SignIn };
49
+ const builtInModels = {
50
+ account: Account,
51
+ appSettings: AppSettings,
52
+ palette: Palette,
53
+ signIn: SignIn
54
+ };
47
55
  const appPathInput = z.custom((value) => typeof value === 'string' && isRoutedPath(value), { error: 'Not a page in src/routes.tsx' });
48
56
  export function defineApp(definition) {
49
57
  const pages = new Set(pageIds(definition.routes));
@@ -58,7 +66,6 @@ export function defineApp(definition) {
58
66
  }
59
67
  return class App {
60
68
  static definition = definition;
61
- name = definition.name;
62
69
  router;
63
70
  models;
64
71
  stores;
@@ -69,28 +76,35 @@ export function defineApp(definition) {
69
76
  #session;
70
77
  #data;
71
78
  #navigations = new Set();
79
+ #repeats = new Set();
80
+ #clock;
72
81
  #failures = [];
73
82
  #path;
74
83
  #user;
75
- constructor(router, { api, cache, gate }) {
84
+ constructor(router, { api, cache, gate, clock = realClock() }) {
76
85
  this.router = router;
77
86
  this.#cache = cache;
78
87
  this.#gate = gate;
88
+ this.#clock = clock;
79
89
  const session = new SessionStore({ api: api.session });
90
+ const app = new AppStore({
91
+ api: api.app,
92
+ remembered: cache.scope('app.title', z.string())
93
+ });
80
94
  const data = dataStores(definition.tables, api.tables, cache, () => viewerOf(session.state.user));
81
95
  const custom = Object.fromEntries(Object.entries(definition.stores ?? {}).map(([key, Store]) => [
82
96
  key,
83
- new Store(cache.persisted(`stores.${key}`))
97
+ new Store(cache.persisted(`stores.${key}`), api.services)
84
98
  ]));
85
- const toasts = new Toasts(cache.persisted('stores.toasts'));
99
+ const toasts = new Toasts(cache.persisted('stores.toasts'), {});
86
100
  const clash = clashOf([
87
- ['session', 'actions', 'toasts', 'page'],
101
+ ['session', 'app', 'actions', 'toasts', 'page'],
88
102
  Object.keys(data.tables),
89
103
  Object.keys(data.publicViews),
90
104
  Object.keys(custom)
91
105
  ]);
92
106
  if (clash !== undefined) {
93
- throw new Error(`Two stores are called "${clash}". The built-in session, actions, toasts and page stores, each table, each table's public view and each store in src/stores/index.ts need their own name.`);
107
+ throw new Error(`Two stores are called "${clash}". The built-in session, app, actions, toasts and page stores, each table, each table's public view and each store in src/stores/index.ts need their own name.`);
94
108
  }
95
109
  const actions = new ActionsStore(this.#page, [...(definition.actions ?? []), ...builtInActions], [
96
110
  ...(definition.actionGroups ?? []),
@@ -98,6 +112,7 @@ export function defineApp(definition) {
98
112
  ], (message) => this.#runAction(message));
99
113
  this.stores = {
100
114
  session,
115
+ app,
101
116
  actions,
102
117
  toasts,
103
118
  page: this.#page,
@@ -111,6 +126,10 @@ export function defineApp(definition) {
111
126
  navigate: (to) => {
112
127
  assertNotBuilding('navigate');
113
128
  return this.#navigate(to);
129
+ },
130
+ every: (ms, run) => {
131
+ assertNotBuilding('repeat');
132
+ return this.#every(ms, run);
114
133
  }
115
134
  };
116
135
  this.models = createModels({ ...builtInModels, ...definition.models }, this.stores, services, (model) => cache.persisted(model));
@@ -128,6 +147,7 @@ export function defineApp(definition) {
128
147
  router.subscribe(() => this.#routeChanged());
129
148
  session.subscribe(() => this.#sessionChanged());
130
149
  void session.confirm();
150
+ void app.load();
131
151
  for (const view of Object.values(data.publicViews))
132
152
  void view.load();
133
153
  this.#loadReadableTables();
@@ -149,6 +169,9 @@ export function defineApp(definition) {
149
169
  stores: snapshotsOf(this.stores)
150
170
  };
151
171
  }
172
+ tick() {
173
+ this.#clock.tick();
174
+ }
152
175
  async whenIdle() {
153
176
  do {
154
177
  await Promise.allSettled(this.#navigations);
@@ -184,11 +207,29 @@ export function defineApp(definition) {
184
207
  this.#navigations.add(navigation);
185
208
  return navigation;
186
209
  }
210
+ #every(ms, run) {
211
+ const stop = this.#clock.every(ms, () => {
212
+ void Promise.resolve(run()).catch((failure) => {
213
+ this.#failures.push(failure);
214
+ });
215
+ });
216
+ const remove = () => {
217
+ stop();
218
+ this.#repeats.delete(remove);
219
+ };
220
+ this.#repeats.add(remove);
221
+ return remove;
222
+ }
223
+ #stopRepeats() {
224
+ for (const stop of [...this.#repeats])
225
+ stop();
226
+ }
187
227
  #routeChanged() {
188
228
  const path = this.router.state.location.pathname;
189
229
  if (path === this.#path)
190
230
  return;
191
231
  this.#path = path;
232
+ this.#stopRepeats();
192
233
  this.#page.moved(this.#currentPage(), this.#currentParams());
193
234
  this.#openCurrentModel();
194
235
  }
@@ -197,6 +238,7 @@ export function defineApp(definition) {
197
238
  if (user === this.#user)
198
239
  return;
199
240
  this.#user = user;
241
+ this.#stopRepeats();
200
242
  this.#cache.clear();
201
243
  for (const store of Object.values(this.#data.tables))
202
244
  store.forget();
@@ -0,0 +1,35 @@
1
+ import type { AppInfo } from './appTitle.ts';
2
+ import type { Cached } from './cache.ts';
3
+ import type { Gate } from './gate.ts';
4
+ import type { MemoryApiOptions } from './memoryApi.ts';
5
+ import { StoreBase } from './store.ts';
6
+ export type AppInfoApi = {
7
+ read: () => Promise<AppInfo>;
8
+ rename: (title: string) => Promise<AppInfo>;
9
+ };
10
+ export type AppInfoCall = 'read' | 'rename';
11
+ export declare const appInfoCalls: readonly AppInfoCall[];
12
+ export declare class HttpAppInfoApi implements AppInfoApi {
13
+ #private;
14
+ constructor(gate: Gate);
15
+ read(): Promise<AppInfo>;
16
+ rename(title: string): Promise<AppInfo>;
17
+ }
18
+ export declare class MemoryAppInfoApi implements AppInfoApi {
19
+ #private;
20
+ constructor(title: string, options: MemoryApiOptions);
21
+ read(): Promise<AppInfo>;
22
+ rename(title: string): Promise<AppInfo>;
23
+ }
24
+ export type AppState = {
25
+ title: string;
26
+ };
27
+ export declare class AppStore extends StoreBase<AppState, {
28
+ api: AppInfoApi;
29
+ remembered: Cached<string>;
30
+ }> {
31
+ #private;
32
+ protected initialState(): AppState;
33
+ load(): Promise<void>;
34
+ rename(title: string): Promise<void>;
35
+ }
@@ -0,0 +1,67 @@
1
+ import { appInfo } from "./appTitle.js";
2
+ import { MemoryServer } from "./memoryApi.js";
3
+ import { StoreBase } from "./store.js";
4
+ export const appInfoCalls = ['read', 'rename'];
5
+ export class HttpAppInfoApi {
6
+ #gate;
7
+ constructor(gate) {
8
+ this.#gate = gate;
9
+ }
10
+ read() {
11
+ return this.#gate.pass('app.read', async () => {
12
+ const response = await fetch('/api/app');
13
+ if (!response.ok)
14
+ throw new Error('app.read failed');
15
+ return appInfo.parse(await response.json());
16
+ });
17
+ }
18
+ rename(title) {
19
+ return this.#gate.pass('app.rename', async () => {
20
+ const response = await fetch('/api/app', {
21
+ method: 'PUT',
22
+ headers: { 'content-type': 'application/json' },
23
+ body: JSON.stringify({ title })
24
+ });
25
+ if (!response.ok)
26
+ throw new Error('app.rename failed');
27
+ return appInfo.parse(await response.json());
28
+ });
29
+ }
30
+ }
31
+ export class MemoryAppInfoApi {
32
+ #server;
33
+ #title;
34
+ constructor(title, options) {
35
+ this.#server = new MemoryServer('app', options);
36
+ this.#title = title;
37
+ }
38
+ read() {
39
+ return this.#server.reply('read', () => ({ title: this.#title }));
40
+ }
41
+ rename(title) {
42
+ return this.#server.reply('rename', () => {
43
+ this.#title = title;
44
+ return { title };
45
+ });
46
+ }
47
+ }
48
+ export class AppStore extends StoreBase {
49
+ initialState() {
50
+ return { title: this.deps.remembered.read() ?? '' };
51
+ }
52
+ async load() {
53
+ try {
54
+ this.#keep(await this.deps.api.read());
55
+ }
56
+ catch {
57
+ return;
58
+ }
59
+ }
60
+ async rename(title) {
61
+ this.#keep(await this.deps.api.rename(title));
62
+ }
63
+ #keep({ title }) {
64
+ this.set({ title });
65
+ this.deps.remembered.write(title);
66
+ }
67
+ }
@@ -0,0 +1,6 @@
1
+ import { z } from 'zod';
2
+ export declare const appTitle: z.ZodString;
3
+ export declare const appInfo: z.ZodObject<{
4
+ title: z.ZodString;
5
+ }, z.core.$strip>;
6
+ export type AppInfo = z.output<typeof appInfo>;
@@ -0,0 +1,3 @@
1
+ import { z } from 'zod';
2
+ export const appTitle = z.string().trim().min(1).max(60);
3
+ export const appInfo = z.object({ title: z.string() });
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+ declare const AppSettings_base: import("../model.ts").ModelClass<import("../registered.ts").AppStores, import("../registered.ts").ModelServices, {
3
+ isRenaming: z.ZodDefault<z.ZodBoolean>;
4
+ }, []>;
5
+ export declare class AppSettings extends AppSettings_base {
6
+ get title(): string;
7
+ get canRename(): boolean;
8
+ rename: import("../model.ts").Command<{
9
+ title: z.ZodString;
10
+ }, Promise<void>>;
11
+ }
12
+ export {};
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ import { appTitle } from "../appTitle.js";
3
+ import { command } from "../model.js";
4
+ import { Model } from "../registered.js";
5
+ export class AppSettings extends Model({
6
+ state: { isRenaming: z.boolean().default(false) }
7
+ }) {
8
+ get title() {
9
+ return this.stores.app.state.title;
10
+ }
11
+ get canRename() {
12
+ return this.stores.session.state.user?.isAdmin === true;
13
+ }
14
+ rename = command({ input: { title: appTitle } }, async ({ title }) => {
15
+ this.set({ isRenaming: true });
16
+ try {
17
+ await this.stores.app.rename(title);
18
+ this.stores.toasts.show({ tone: 'success', title: 'Name saved' });
19
+ }
20
+ catch {
21
+ this.stores.toasts.show({
22
+ tone: 'error',
23
+ title: 'Couldn’t save the name',
24
+ description: 'Check your connection and try again.'
25
+ });
26
+ }
27
+ finally {
28
+ this.set({ isRenaming: false });
29
+ }
30
+ });
31
+ }
@@ -24,7 +24,7 @@ declare const Toasts_base: import("../store.ts").StoreClass<{
24
24
  title: z.ZodString;
25
25
  description: z.ZodNullable<z.ZodString>;
26
26
  }, z.core.$strip>>>;
27
- }, []>;
27
+ }, [], Record<never, never>>;
28
28
  export declare class Toasts extends Toasts_base {
29
29
  show({ description, ...shown }: ToastInput): void;
30
30
  dismiss(id: string): void;
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { Store } from "../store.js";
2
+ import { defineStore } from "../store.js";
3
+ const Store = defineStore();
3
4
  const toast = z.object({
4
5
  id: z.string(),
5
6
  tone: z.enum(['neutral', 'success', 'error']),