From b14c76cfef052ed0f33636907ab164b251bfd760 Mon Sep 17 00:00:00 2001 From: Offirmo Date: Mon, 30 Sep 2024 20:51:38 +1000 Subject: [PATCH] +++ --- .../senior-dev/tech-bites--frontend-web.md | 1 + .../lists/senior-dev/tech-bites--system.md | 2 + .../active/view--chat/package.json | 1 + .../src/__fixtures/primitives--console.ts | 2 +- .../src/__fixtures/todo-pretend-server.txt | 0 .../active/view--chat/src/__fixtures/tour.ts | 36 ++-- .../view--chat/src/implementation/types.ts | 19 +- .../active/view--chat/src/loop/index.ts | 178 +++++++++++------- .../active/view--chat/src/types/types.ts | 35 ++-- 9 files changed, 170 insertions(+), 104 deletions(-) create mode 100644 stack--current/5-incubator/active/view--chat/src/__fixtures/todo-pretend-server.txt diff --git a/stack--current/5-incubator/active--no-pkg/bite-sized/lists/senior-dev/tech-bites--frontend-web.md b/stack--current/5-incubator/active--no-pkg/bite-sized/lists/senior-dev/tech-bites--frontend-web.md index fe4136d9..e02ea327 100644 --- a/stack--current/5-incubator/active--no-pkg/bite-sized/lists/senior-dev/tech-bites--frontend-web.md +++ b/stack--current/5-incubator/active--no-pkg/bite-sized/lists/senior-dev/tech-bites--frontend-web.md @@ -37,6 +37,7 @@ bot detection browsing contexts = such as several windows, iframes or even workers cache https://csswizardry.com/2024/08/cache-grab-how-much-are-you-leaving-on-the-table/ CDN +CDN -- quality = hot/cold potato routing https://vercel.com/blog/latency-numbers-every-web-developer-should-know chrome = the graphical framework and elements surrounding the content. means different things depending on the context: In the context of a web browser it is the navigation, toolbar etc. In the context of a website it is navigation, ad-space and other fixed aspects of the design https://stackoverflow.com/a/5072092/587407 Chrome is not the standard https://v4.chriskrycho.com/2017/chrome-is-not-the-standard.html client/server -- multi-tier architecture -- 01 presentation 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 f8d36e94..1caef088 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 @@ -245,6 +245,8 @@ kafka = pub/sub + store + process 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/ +latency numbers -- frontend -- duration of time perceived by humans as sluggish = 200ms. a reaction response slower than this value will be perceived as having to wait +latency numbers -- frontend -- shortest duration of time perceived by humans as time having passed = 40-80ms. response below this duration means that your user will perceive the response as instant latency numbers -- frontend https://vercel.com/blog/latency-numbers-every-web-developer-should-know latency numbers https://brenocon.com/dean_perf.html limiting -- content limiting = ex. only X Gb of storage diff --git a/stack--current/5-incubator/active/view--chat/package.json b/stack--current/5-incubator/active/view--chat/package.json index 9d6ddb82..66cf573d 100644 --- a/stack--current/5-incubator/active/view--chat/package.json +++ b/stack--current/5-incubator/active/view--chat/package.json @@ -36,6 +36,7 @@ "demo": "node ./dist/src.es2023.esm/loop/demo.js" }, "devDependencies": { + "@offirmo/deferred": "*", "npm-run-all": "^4", "tslib": "^2" } diff --git a/stack--current/5-incubator/active/view--chat/src/__fixtures/primitives--console.ts b/stack--current/5-incubator/active/view--chat/src/__fixtures/primitives--console.ts index 86734391..71299c86 100644 --- a/stack--current/5-incubator/active/view--chat/src/__fixtures/primitives--console.ts +++ b/stack--current/5-incubator/active/view--chat/src/__fixtures/primitives--console.ts @@ -28,7 +28,7 @@ const CHAT_CONSOLE: ChatPrimitives = { display_task: ({ msg_before, - progress_promise, + promise, msg_after, }) => { throw new Error(`NO UI display_task!`) diff --git a/stack--current/5-incubator/active/view--chat/src/__fixtures/todo-pretend-server.txt b/stack--current/5-incubator/active/view--chat/src/__fixtures/todo-pretend-server.txt new file mode 100644 index 00000000..e69de29b diff --git a/stack--current/5-incubator/active/view--chat/src/__fixtures/tour.ts b/stack--current/5-incubator/active/view--chat/src/__fixtures/tour.ts index ecdf2904..f9c2cadb 100755 --- a/stack--current/5-incubator/active/view--chat/src/__fixtures/tour.ts +++ b/stack--current/5-incubator/active/view--chat/src/__fixtures/tour.ts @@ -1,14 +1,20 @@ import { type Step, StepType } from '../types/types.js' import { type StepsGenerator } from '../loop/types.js' +import Deferred from '@offirmo/deferred' export default function* get_next_step(skip_to_index: number = 0) { + console.log('get_next_step()', { skip_to_index }) + const state = { mode: 'main', name: undefined, city: undefined, } - const STEPS: Array = [ + const warmup_promise = new Deferred() + setTimeout(() => warmup_promise.reject(new Error('Failed!')), 3000) + + const STEPS: Array> = [ { type: StepType.perceived_labor, @@ -22,17 +28,18 @@ export default function* get_next_step(skip_to_index: number = 0) { type: StepType.progress, msg_before: 'Warming up...', - task_promise: (new Promise((resolve, reject) => setTimeout(() => reject(new Error('Demo step 2 rejection!')), 2000))), + promise: warmup_promise, msg_after: success => success ? '✔ Ready!' : '❌ Warm up unsuccessful.', callback: success => console.log(`[callback called: ${success}]`), }, - /* { - type: 'simple_message', - msg_main: 'Welcome. I’ll have a few questions…', + type: StepType.simple_message, + msg: 'Welcome. I’ll have a few questions…', }, + + /* { type: 'ask_for_string', msg_main: 'What’s your name?', @@ -48,15 +55,18 @@ export default function* get_next_step(skip_to_index: number = 0) { msgg_acknowledge: value => `${value}, a fine city indeed!`, callback: value => { state.city = value }, }, + */ + { - type: 'simple_message', - msg_main: 'Please wait for a moment...', + type: StepType.simple_message, + msg: 'Please wait for a moment...', }, { - type: 'progress', + type: StepType.perceived_labor, + msg_before: 'Calling server...', duration_ms: 1000, - msg_main: 'Calling server...', }, + /* { msg_main: 'Please choose between 1 and 2?', callback: value => { state.mode = value }, @@ -70,11 +80,11 @@ export default function* get_next_step(skip_to_index: number = 0) { value: 2, }, ], - }, - { - type: 'simple_message', - msg_main: 'Thanks, good bye.', },*/ + { + type: StepType.simple_message, + msg: 'Thanks, good bye.', + }, ] yield* STEPS.slice(skip_to_index) diff --git a/stack--current/5-incubator/active/view--chat/src/implementation/types.ts b/stack--current/5-incubator/active/view--chat/src/implementation/types.ts index 03e29841..643b8881 100644 --- a/stack--current/5-incubator/active/view--chat/src/implementation/types.ts +++ b/stack--current/5-incubator/active/view--chat/src/implementation/types.ts @@ -2,30 +2,37 @@ import { Enum } from 'typescript-string-enums' import { PProgress as PromiseWithProgress } from 'p-progress' import { type Immutable } from '@offirmo-private/ts-types' +import { type TaskProgressStep } from '../types/types.js' + ///////////////////////////////////////////////// +// primitives should always accept string = common lowest denominator +// up to it to convert to rich text if needed interface ChatPrimitives { setup(): Promise // core primitives - display_message(p: {msg: ContentType, choices?: ContentType[]}): Promise + display_message(p: { + msg: ContentType | string, + choices?: Array + }): Promise // a staple of chat interfaces // to be used between steps pretend_to_think(p: {duration_ms: number}): Promise pretend_to_work(p: { - msg_before: ContentType, + msg_before: ContentType | string, duration_ms: number, - msg_after: ContentType, + msg_after: ContentType | string, }): Promise //read_answer(step) TODO clarify display_task(p: { - msg_before: ContentType, - progress_promise: PromiseWithProgress, - msg_after: ContentType, + msg_before: TaskProgressStep['msg_before'], + promise: TaskProgressStep['promise'], + msg_after: TaskProgressStep['msg_after'], }): Promise // while we wait for the next step. diff --git a/stack--current/5-incubator/active/view--chat/src/loop/index.ts b/stack--current/5-incubator/active/view--chat/src/loop/index.ts index e224af50..34cee362 100644 --- a/stack--current/5-incubator/active/view--chat/src/loop/index.ts +++ b/stack--current/5-incubator/active/view--chat/src/loop/index.ts @@ -2,136 +2,142 @@ import is_promise from 'is-promise' import { type Immutable } from '@offirmo-private/ts-types' import { type ChatPrimitives } from '../implementation/types.js' -import { type Step } from '../types/index.js' +import { type Step, StepType } from '../types/index.js' import { create_dummy_progress_promise } from '../utils/index.js' import { StepsGenerator } from './types.js' ///////////////////////////////////////////////// interface Options { - DEBUG: boolean gen_next_step: StepsGenerator primitives: ChatPrimitives - //inter_msg_delay_ms: number - //after_input_delay_ms: number - //to_prettified_str: (x: any) => string + + // TODO review! merge? + inter_msg_delay_ms?: number // standard time between steps + after_input_delay_ms?: number // time we should pretend to process the user input + + DEBUG?: boolean + DEBUG_to_prettified_str?: (x: any) => any } +const LIB = 'chat_loop' + +// TODO one day expose state for React +// state = err | step | answer | progress... + function create({ - DEBUG, gen_next_step, primitives, - //inter_msg_delay_ms = 0, - //after_input_delay_ms = 0, - //to_prettified_str = x => x, // work with browser + inter_msg_delay_ms = 0, + after_input_delay_ms = 0, // TODO better defaults + DEBUG = false, + DEBUG_to_prettified_str = (x: any) => x, // work with browser }: Options) { - if (DEBUG) console.log('↘ chat_loop.create()') + if (DEBUG) console.log(`↘ ${LIB}.create()`) async function start() { - if (DEBUG) console.log('↘ chat_loop.start()') + if (DEBUG) console.log(`↘ ${LIB}.start()`) try { await primitives.setup() let should_exit = false - let last_step = undefined // just in case - let last_answer = undefined // just in case + let last_step = undefined + let last_answer = undefined + do { const step_start_timestamp_ms = +new Date() const raw_yielded_step = gen_next_step.next({last_step, last_answer}) - console.log('raw_yielded_step', raw_yielded_step) + //console.log(`[${LIB}]`, { raw_yielded_step }) // TODO can be a promise? const yielded_step: any = is_promise(raw_yielded_step) ? await primitives.spin_until_resolution(raw_yielded_step as any) : raw_yielded_step - console.log('yielded_step', yielded_step) + //console.log(`[${LIB}]`, {yielded_step}) const { value: raw_step, done } = yielded_step if (done) { should_exit = true continue } + const step: Step = raw_step - console.log('about to execute step', raw_step) - throw new Error(`NIMP!`) - - /* - const step = normalize_step(raw_step) + // TODO process the separation with the previous step const elapsed_time_ms = (+new Date()) - step_start_timestamp_ms + /* if (is_step_input(last_step)) { // pretend to have processed the user answer await primitives.pretend_to_think(Math.max(0, after_input_delay_ms - elapsed_time_ms)) } + */ - last_answer = await execute_step(step) + //console.log(`[${LIB}] about to execute step`, step) + const answer = await execute_step(step) + + last_answer = answer last_step = step - */ } while (!should_exit) } catch (err) { - if (DEBUG) console.error('chat_loop encountered error:', err) + if (DEBUG) console.error(`[${LIB}] encountered error:`, err) throw err } finally { await primitives.teardown() } } -/* - async function ask_user(step) { - if (DEBUG) console.log('↘ ask_user(\n', to_prettified_str(step, {outline: true}), '\n)') - - let answer = '' - const ok = true // TODO used for confirmation - do { - await primitives.display_message({msg: step.msg_main, choices: step.choices}) - answer = await primitives.read_answer(step) - if (DEBUG) console.log(`↖ ask_user(…) answer = "${answer}"`) - } while (!ok) - await primitives.pretend_to_think(after_input_delay_ms) - let acknowledged = false - if (step.choices.length) { - const selected_choice = step.choices.find(choice => choice.value === answer) - if (selected_choice.msgg_acknowledge) { - await primitives.display_message({msg: selected_choice.msgg_acknowledge(answer)}) - acknowledged = true - } - } - if (!acknowledged && step.msgg_acknowledge) { - await primitives.display_message({msg: step.msgg_acknowledge(answer)}) - acknowledged = true - } - if (!acknowledged) { - // Fine! It's optional. - if (DEBUG) console.warn('You may want to add an acknowledge message to this step.') - } - - return answer - } + async function execute_step(step: Step) { + if (DEBUG) console.log('↘ ${LIB}.execute_step(\n', DEBUG_to_prettified_str(step), '\n)') - async function execute_step(step) { - if (DEBUG) console.log('↘ execute_step(\n', to_prettified_str(step, {outline: true}), '\n)') + //const step = normalize_step(raw_step) switch (step.type) { - case 'simple_message': - await primitives.pretend_to_think(inter_msg_delay_ms) - await primitives.display_message({ msg: step.msg_main }) + case StepType.simple_message: + await primitives.pretend_to_think({duration_ms: inter_msg_delay_ms}) + await primitives.display_message({ msg: step.msg }) break - case 'progress': - await primitives.display_progress({ - progress_promise: step.progress_promise - || create_dummy_progress_promise({ DURATION_MS: step.duration_ms }), - msg: step.msg_main, - msgg_acknowledge: step.msgg_acknowledge, + case StepType.perceived_labor: + await primitives.pretend_to_work({ + msg_before: step.msg_before || 'Please wait…', + duration_ms: step.duration_ms || 1200, // 200ms = minimum time to be perceived as work + msg_after: step.msg_after || 'Done!', }) - .then(() => true, () => false) + break + + case StepType.progress: { + let result: any = undefined + let error: Error | undefined = undefined + + await primitives.display_task({ + msg_before: step.msg_before || 'Processing…', + promise: step.promise, + msg_after: step.msg_after || ((success: boolean, result: any) => { + if (success) + return 'Done!' + else + return `Failed! ("${result?.message}")` + }), + }) + .then( + (_res) => { + result = _res + return true + }, + (_err) => { + error = _err as any // TODO one day coerce to error using error utils + return false + }) .then(success => { if (step.callback) - step.callback(success) + step.callback(success, result || error) }) break + } + +/* case 'ask_for_confirmation': case 'ask_for_string': @@ -157,13 +163,47 @@ function create({ throw err } return answer - } + }*/ default: throw new Error(`Unsupported step type: "${step.type}"!`) } } - */ + +/* + async function ask_user(step) { + if (DEBUG) console.log('↘ ask_user(\n', to_prettified_str(step, {outline: true}), '\n)') + + let answer = '' + const ok = true // TODO used for confirmation + do { + await primitives.display_message({msg: step.msg_main, choices: step.choices}) + answer = await primitives.read_answer(step) + if (DEBUG) console.log(`↖ ask_user(…) answer = "${answer}"`) + } while (!ok) + await primitives.pretend_to_think(after_input_delay_ms) + + let acknowledged = false + if (step.choices.length) { + const selected_choice = step.choices.find(choice => choice.value === answer) + if (selected_choice.msgg_acknowledge) { + await primitives.display_message({msg: selected_choice.msgg_acknowledge(answer)}) + acknowledged = true + } + } + if (!acknowledged && step.msgg_acknowledge) { + await primitives.display_message({msg: step.msgg_acknowledge(answer)}) + acknowledged = true + } + if (!acknowledged) { + // Fine! It's optional. + if (DEBUG) console.warn('You may want to add an acknowledge message to this step.') + } + + return answer + } +*/ + return { start, } diff --git a/stack--current/5-incubator/active/view--chat/src/types/types.ts b/stack--current/5-incubator/active/view--chat/src/types/types.ts index 1a9fc8d0..5fc08a03 100644 --- a/stack--current/5-incubator/active/view--chat/src/types/types.ts +++ b/stack--current/5-incubator/active/view--chat/src/types/types.ts @@ -24,15 +24,15 @@ interface BaseStep { interface SimpleMessageStep extends BaseStep { type: typeof StepType.simple_message - msg: ContentType + msg: ContentType | string } interface PerceivedLaborStep extends BaseStep { type: typeof StepType.perceived_labor - msg_before?: ContentType + msg_before?: ContentType | string duration_ms?: number - msg_after?: ContentType + msg_after?: ContentType | string // callback? } @@ -40,11 +40,11 @@ interface PerceivedLaborStep extends BaseStep { interface TaskProgressStep extends BaseStep { type: typeof StepType.progress - msg_before?: ContentType - task_promise: Promise | PromiseWithProgress - msg_after?: (success: boolean, result: T | Error) => ContentType + msg_before?: ContentType | string + promise: Promise | PromiseWithProgress + msg_after?: (success: boolean, result: T | Error) => ContentType | string - callback: (success: boolean, result: T | Error) => void + callback?: (success: boolean, result: T | Error) => void } // TODO merge with input? @@ -52,9 +52,9 @@ interface AskForConfirmationStep extends BaseStep { type: typeof StepType.ask_for_confirmation prompt?: string - msg_after?: (confirmation: boolean) => ContentType + msg_after?: (confirmation: boolean) => ContentType | string - callback: (confirmation: boolean) => void + callback?: (confirmation: boolean) => void } // TODO refine @@ -63,16 +63,16 @@ interface AskForConfirmationStep extends BaseStep { interface InputStep extends BaseStep { type: typeof StepType.input - prompt: ContentType + prompt: ContentType | string normalizer?: (raw: T) => T - msg_as_user: (value: T) => ContentType - validators: Array<(value: T) => [ boolean, ContentType ]> - msg_acknowledge: (value: T) => ContentType + msg_as_user: (value: T) => ContentType | string + validators: Array<(value: T) => [ boolean, ContentType | string ]> + msg_acknowledge: (value: T) => ContentType | string - callback: (value: T) => void + callback?: (value: T) => void } -type Step = +type Step = | SimpleMessageStep | PerceivedLaborStep | TaskProgressStep @@ -82,6 +82,11 @@ type Step = ///////////////////////////////////////////////// export { + type SimpleMessageStep, + type PerceivedLaborStep, + type TaskProgressStep, + type AskForConfirmationStep, + type InputStep, type Step, // for convenience