diff --git a/stack--current/3-advanced--multi/rich-text-format/src/renderers/to_actions.ts b/stack--current/3-advanced--multi/rich-text-format/src/renderers/to_actions.ts index b1274f9d..51720b22 100644 --- a/stack--current/3-advanced--multi/rich-text-format/src/renderers/to_actions.ts +++ b/stack--current/3-advanced--multi/rich-text-format/src/renderers/to_actions.ts @@ -1,3 +1,6 @@ +import { type URI‿x } from '@offirmo-private/ts-types-web' +import { type Hyperlink } from '@offirmo-private/ts-types-web' + import { BaseRenderingOptions, OnConcatenateStringParams, @@ -11,33 +14,82 @@ import { Node, CheckedNode } from '../types.js' ///////////////////////////////////////////////// -interface Action { - $node: CheckedNode - type: 'link' | undefined - data: any - // priority ? - // TODO more +interface BaseAction { + $node: CheckedNode // the node where this action was found +} + +interface HyperlinkAction extends BaseAction { + type: 'hyperlink' + data: Hyperlink +} + +interface EmbeddedReducerAction { + cta?: string // optional bc should ideally be derived from the action (esp. for i18n) BUT same action could have different CTA following the context (ex. equip best equipment) + data: any // the data of the action, could be anything +} +interface ReducerAction extends BaseAction, EmbeddedReducerAction { + type: 'action' // in the sense of reducer(action) +} + +type Action = + | HyperlinkAction + | ReducerAction + +interface RenderingOptionsⵧToActions extends BaseRenderingOptions { + getꓽactions: (node: CheckedNode) => Action[] // will be executed on every node } -interface RenderingOptionsⵧToActions extends BaseRenderingOptions {} const DEFAULT_RENDERING_OPTIONSⵧToActions= Object.freeze({ shouldꓽrecover_from_unknown_sub_nodes: false, + getꓽactions: ($node: CheckedNode): Action[] => { + const actions: Action[] = [] + + if ($node.$hints['href']) { + actions.push({ + $node, + type: 'hyperlink', + data: { + // TODO default CTA from $node itself? + href: $node.$hints['href'], // TODO escaping for security? (This is debug, see React renderer which will do) + rel: [], + }, + }) + } + + if ($node.$hints['actions']) { + actions.push(...$node.$hints['actions'].map((action: EmbeddedReducerAction): ReducerAction => { + return { + // TODO default CTA from $node itself? + ...action, + $node, + type: 'action', + } + })) + } + + if ($node.$hints['links']) { + actions.push(...Object.values($node.$hints['links']).map((link: Hyperlink): HyperlinkAction => { + return { + $node, + type: 'hyperlink', + data: link, + } + })) + } + + return actions + }, }) type State = { actions: Action[], } -const on_type: WalkerReducer, RenderingOptionsⵧToActions> = ({$type, state, $node, depth}) => { + +const on_type: WalkerReducer, RenderingOptionsⵧToActions> = ({$type, state, $node, depth}, { getꓽactions }) => { //console.log('[on_type]', { $type, state }) - if ($node.$hints.href) { - state.actions.push({ - $node, - type: 'link', - data: $node.$hints.href, // TODO escaping for security? (This is debug, see React renderer which will do) - }) - } + state.actions.push(...getꓽactions($node)) return state } @@ -67,9 +119,14 @@ function renderⵧto_actions($doc: Node, options: RenderingOptionsⵧToActions = ///////////////////////////////////////////////// export { + type HyperlinkAction, + type ReducerAction, type EmbeddedReducerAction, type Action, type RenderingOptionsⵧToActions, + DEFAULT_RENDERING_OPTIONSⵧToActions, + renderⵧto_actions, + callbacksⵧto_actions, } diff --git a/stack--current/3-advanced/ts--types--web/src/01-links/types.ts b/stack--current/3-advanced/ts--types--web/src/01-links/types.ts index 0b729f77..bf10e014 100644 --- a/stack--current/3-advanced/ts--types--web/src/01-links/types.ts +++ b/stack--current/3-advanced/ts--types--web/src/01-links/types.ts @@ -61,7 +61,7 @@ interface SchemeSpecificURIPart { /** (optional) the immediate parent in the cascade. Useful to resolve the full URI IF NEEDED * TODO clarify how to set a "encapsulating URI" for properly detecting and resolving relative URIs */ - parent?: SchemeSpecificURIPart + //parent?: SchemeSpecificURIPart } type URI‿x = Uri‿str | SchemeSpecificURIPart diff --git a/stack--current/5-incubator/active--no-pkg/bite-sized/lists/senior-dev/tech-bites--concepts.md b/stack--current/5-incubator/active--no-pkg/bite-sized/lists/senior-dev/tech-bites--concepts.md index a8fdf125..dd01cca0 100644 --- a/stack--current/5-incubator/active--no-pkg/bite-sized/lists/senior-dev/tech-bites--concepts.md +++ b/stack--current/5-incubator/active--no-pkg/bite-sized/lists/senior-dev/tech-bites--concepts.md @@ -23,6 +23,7 @@ [ ] https://www.managingtheunmanageable.net/morerulesofthumb.html [ ] https://www.tedinski.com/archive/ [ ] https://zknill.io/posts/every-programmer-should-know/ +[ ] SWE at Google https://abseil.io/resources/swe-book/html/toc.html [ ] technical things every software developer should know https://github.com/mtdvio/every-programmer-should-know abstraction https://www.merrickchristensen.com/articles/abstraction/ access control -- Role-Based (RBAC) @@ -89,6 +90,7 @@ commits -- conventional https://www.conventionalcommits.org/ https://gist.gith compile to js compiler compiler -- stanford course http://openclassroom.stanford.edu/MainFolder/CoursePage.php?course=Compilers +complexity budget https://htmx.org/essays/complexity-budget/ compute compute -- hosted compute concurrency -- ABA problem @@ -290,6 +292,7 @@ linked list linting locality locality -- conflict locality = reducing conflicts on close changes, ex. enforcing trailing commas, sorting... +locality -- of behaviour "LoB" https://htmx.org/essays/locality-of-behaviour/ lodash log loop diff --git a/stack--current/5-incubator/active--no-pkg/bite-sized/lists/senior-dev/tech-bites--system.md b/stack--current/5-incubator/active--no-pkg/bite-sized/lists/senior-dev/tech-bites--system.md index 28dbc94f..92a57f19 100644 --- a/stack--current/5-incubator/active--no-pkg/bite-sized/lists/senior-dev/tech-bites--system.md +++ b/stack--current/5-incubator/active--no-pkg/bite-sized/lists/senior-dev/tech-bites--system.md @@ -4,18 +4,20 @@ 3 nines / 4 nines / 5 nines -- 3 nines == 99.9% uptime 5 nines == 99.999% uptime, this means your service is down less than 6 min in a year! [ ] +++ https://github.com/donnemartin/system-design-primer [ ] +++ https://lethain.com/distributed-systems-vocabulary/ +[ ] API guidelines https://github.com/microsoft/api-guidelines/blob/vNext/graph/Guidelines-deprecated.md [ ] architecture https://engineering.fb.com/2020/08/17/production-engineering/async/ [ ] client side API gateway "egress" TODO [ ] concepts https://dgraph.io/docs/design-concepts/ [ ] great articles https://dgraph.io/blog/ [ ] https://carloarg02.medium.com/how-i-scaled-amazons-load-generator-to-run-on-1000s-of-machines-4ca8f53812cf [ ] https://discord.com/blog/how-discord-stores-trillions-of-messages +[ ] https://github.com/microsoft/api-guidelines/blob/vNext/azure/ConsiderationsForServiceDesign.md +[ ] https://htmx.org/essays/ [ ] https://medium.com/@sureshpodeti/system-design-twitter-a98e7d134634 [ ] https://newsletter.pragmaticengineer.com/p/building-the-threads-app [ ] https://slack.engineering/scaling-datastores-at-slack-with-vitess/ [ ] https://www.infoq.com/news/2024/01/discord-midjourney-performance/ [ ] https://www.youtube.com/watch?v=paTtLhZFsGE -[ ] kubernetes [ ] microservice -- design -- https://www.salesforce.com/blog/microservice-design-principles/ [ ] paper "Adding new protocols to the cloud native ecosystem" https://docs.google.com/document/d/13wFFC7vIdB2hkxdyT0dSiGgkZTXCDDBeW_GBPqy9Jy0/edit [ ] paper https://research.google/pubs/large-scale-cluster-management-at-google-with-borg/ @@ -37,6 +39,7 @@ ACID = a set of properties of database transactions intended to guarantee data v API -- contracts API -- Diogo Lucas classification = far-team, near-team and inner-team. inner team = used within a team or between partner teams, near team = "Conway APIs" between teams that don't always communicate with each other, Far-team API = between teams that have low-bandwidth communications API -- hybrids = reconcile the need for a stable public API that will not constantly disrupt your carefully tended ecosystem with your team’s ability to move fast and (eventually) break stuff? cater for different requirements regarding security and SLAs or to radically different client natures? https://www.youtube.com/watch?v=eqy609JleoE +API -- REST -- POST https://www.tbray.org/ongoing/When/200x/2009/03/20/Rest-Casuistry API -- visibility = public/external > internal > team > private beta. Guidelines: Consider a public-by-default approach, even if you are starting at a lower level of access (helps dogfooding, new use cases. Treat your internal APIs as candidates for Public level access (bc happens quickly, sometimes for merging with a public one) API gateway API-First = Built with APIs from the ground up. All functionality is exposed through an API @@ -252,6 +255,7 @@ infrastructure as code (IaC) https://bluelight.co/blog/best-infrastructure-as-co ingress/egress = “the act of entering”, “the right of entering”, or “the means of entering” integrity kafka = pub/sub + store + process +kubernetes lambdalith https://rehanvdm.com/blog/should-you-use-a-lambda-monolith-lambdalith-for-the-api latency = the time that passes between an action and the resulting response latency https://www.a10networks.com/glossary/osi-network-model-and-types-of-load-balancers/ diff --git a/stack--current/5-incubator/active--no-pkg/bite-sized/lists/serious-memes/mental-models.md b/stack--current/5-incubator/active--no-pkg/bite-sized/lists/serious-memes/mental-models.md index 238e2d05..12d69a00 100644 --- a/stack--current/5-incubator/active--no-pkg/bite-sized/lists/serious-memes/mental-models.md +++ b/stack--current/5-incubator/active--no-pkg/bite-sized/lists/serious-memes/mental-models.md @@ -167,6 +167,7 @@ hawks and doves https://seldo.com/posts/the_case_for_frameworks helicopter parent herbe toujours plus verte Hickam’s Dictum: The opposite of Occam’s Razor. In a complex system, problems usually have more than one cause. For example, in medicine, people can have many diseases at the same time. +high school "What You'll Wish You'd Known" https://paulgraham.com/hs.html high/low functioning human Hock Principle: Simple, clear purpose and principles give rise to complex and intelligent behavior. Complex rules and regulations give rise to simple and stupid behavior. Hofstede's cultural dimensions theory https://en.wikipedia.org/wiki/Hofstede%27s_cultural_dimensions_theory diff --git a/stack--current/A-apps--core/the-boring-rpg/xx-sandbox/src/hateoas/index.ts b/stack--current/A-apps--core/the-boring-rpg/xx-sandbox/src/hateoas/index.ts index 383c9f72..6a2a0506 100644 --- a/stack--current/A-apps--core/the-boring-rpg/xx-sandbox/src/hateoas/index.ts +++ b/stack--current/A-apps--core/the-boring-rpg/xx-sandbox/src/hateoas/index.ts @@ -1,5 +1,6 @@ +import assert from 'tiny-invariant' import { Immutable } from '@offirmo-private/state-utils' -import { type Hyperlink } from '@offirmo-private/ts-types-web' +import { type Hyperlink, type URI‿x, type SchemeSpecificURIPart } from '@offirmo-private/ts-types-web' import * as RichText from '@offirmo-private/rich-text-format' import * as State from '@tbrpg/state' @@ -11,12 +12,13 @@ import { create, StepType, type Step, + type SelectStep, type StepIterator, type StepIteratorTNext, type StepIteratorYieldResult, type StepIteratorReturnResult, } from '@offirmo-private/view--chat' -import { create_action, type ActionPlay } from '@tbrpg/interfaces' +import { type Action, create_action, ActionType, type ActionPlay } from '@tbrpg/interfaces' ///////////////////////////////////////////////// // TODO async to pretend we're talking to a server (even if it's a worker) @@ -40,24 +42,79 @@ class Game { ///////////////////////////////////////////////// -function HATEOASᐧGET(state: Immutable, url: Hyperlink['href'] = ''): RichText.Document { +function normalizeꓽurl(url: URI‿x): SchemeSpecificURIPart { + url ||= '/' + + if (typeof url === 'string') { + const url‿obj = new URL(url, 'https://example.com') + + const result: SchemeSpecificURIPart = { + path: url‿obj.pathname, + + query: url‿obj.search, + + ...(url‿obj.hash && { fragment: url‿obj.hash }), + } + + return result + } + + return url +} + +// https://htmx.org/essays/hateoas/ +// https://restfulapi.net/hateoas/ +function HATEOASᐧGET(state: Immutable, url: Hyperlink['href'] = '/'): RichText.Document { console.log(`↘ HATEOASᐧGET("${url}")`) - // TODO URL normalization (one day) + const { path, query, fragment } = normalizeꓽurl(url) let $builder = RichText.fragmentⵧblock() + const links: { [key: string]: Hyperlink } = {} + const actions: Array = [] + + // for being pedantic HATEOAS :) + const self: Hyperlink = { + href: url, // TODO after normalize? only path? + rel: ['self'], + cta: '…', // TODO later + } + links['self'] = self + + // ROOT links (always available) + const home: Hyperlink = { + href: '/', + rel: ['home', 'tabsⵧrootⵧ01'], + cta: 'Home', + } + links['rootⵧhome'] = home + const modeⵧequipment: Hyperlink = { + href: '/equipment/', + rel: ['tabsⵧrootⵧ02'], + cta: 'Manage equipment & inventory…', + } + links['rootⵧequipment'] = modeⵧequipment + const modeⵧcharacter_sheet: Hyperlink = { + href: '/character/', + rel: ['tabsⵧrootⵧ03'], + cta: 'Manage character & attributes…', + } + links['rootⵧcharacter_sheet'] = modeⵧcharacter_sheet + const modeⵧachievements: Hyperlink = { + href: '/achievements/', + rel: ['tabsⵧrootⵧ04'], + cta: 'Review achievements…', + } + links['rootⵧachievements'] = modeⵧachievements + // TODO social & shopping + + // TODO recursive routing! + // TODO "back" + + switch (path) { + case '/': { // home + self.cta = 'Explore' - /*.pushNode(RichText.heading().pushText('Identity:').done(), {id: 'header'}) - .pushNode( - RichText.listⵧunordered() - .pushKeyValue('name', $doc_name) - .pushKeyValue('class', $doc_class) - .done() - ) - .done()*/ - - switch (url) { - case '': { // home $builder = $builder.pushHeading('Currently adventuring…') // NO recap of last adventure, it's a different route? @@ -67,6 +124,7 @@ function HATEOASᐧGET(state: Immutable, url: Hyperlink['href'] = ' $builder = $builder.pushNode( RichText.strong().pushText('Your inventory is full!').done(), ) + // TODO add action to sell items? } if(State.getꓽavailable_energy‿float(state.t_state) >= 1) { @@ -76,26 +134,29 @@ function HATEOASᐧGET(state: Immutable, url: Hyperlink['href'] = ' $builder = $builder.pushBlockFragment('You can play again in ' + State.getꓽhuman_time_to_next_energy(state)) } - // actions! - // 1. we're home so links to other "modes/mechanics" - const modeⵧequipment: Hyperlink = { - href: '/equipment', - cta: 'Manage equipment', - } - $builder = $builder.addHints({ - + // actions related to the current mode + // but not root (already declared) + const actionⵧplay = create_action({ + type: ActionType.play, + expected_revisions: {}, + }) + actions.push({ + cta: 'Play!', + data: actionⵧplay }) - // 2. links related to the current mode - const actionⵧplay = create_action() break } - case '/adventure': // ?? default: - throw new Error(`Unknown resource locator "${url}"!`) + throw new Error(`Unknown resource locator "${path}"!`) } + $builder = $builder.addHints({ + links, + actions, + }) + return $builder.done() } @@ -107,7 +168,7 @@ class GameChat implements StepIterator { game: Game = new Game() status: 'starting' | 'normal' = 'starting' is_done: boolean = false - current_route = '' + current_route = normalizeꓽurl('/').path non_interactive_steps_count = 0 constructor() { @@ -182,7 +243,35 @@ class GameChat implements StepIterator { msg: hypermedia, } - // TODO extract actions! + const actions = RichText.renderⵧto_actions(hypermedia) + if (!actions.length) { + // should never happen, we should always have home/root + throw new Error('No actions found!') + } + + const actionsⵧreducers = actions.filter(a => a.type === 'action') + const actionsⵧlinks = actions + .filter(a => a && a.type === 'hyperlink') + .filter((ha: RichText.HyperlinkAction)=> { + return !ha.data.rel.includes('self') + && normalizeꓽurl(ha.data.href).path !== this.current_route + }) + console.log(actionsⵧreducers) + console.log(actionsⵧlinks) + assert((actionsⵧreducers.length + actionsⵧlinks.length) > 0, `We should have actions for continuing!`) + + const actions_step: SelectStep = { + type: StepType.select, + prompt: 'What do you want to do?', + options: { + yes: { value: true }, + no: { value: false }, + }, + /*msg_as_user: (action: RichText.Action) => confirm ? `Yes, I confirm.` : `No, I cancel.`, + msg_acknowledge: (action: RichText.Action) => confirm ? `Ok, let's proceed ✔` : `Let's cancel that ✖`, + ...parts, + */ + } /* if (State.is_inventory_full(state.u_state)) {