{"id":50439287,"url":"https://github.com/ortegavan/operators","last_synced_at":"2026-05-31T18:04:06.839Z","repository":{"id":344330192,"uuid":"1181413752","full_name":"ortegavan/operators","owner":"ortegavan","description":null,"archived":false,"fork":false,"pushed_at":"2026-03-14T16:31:50.000Z","size":113,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-14T16:40:09.230Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ortegavan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-14T05:25:18.000Z","updated_at":"2026-03-14T16:31:54.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ortegavan/operators","commit_stats":null,"previous_names":["ortegavan/operators"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/ortegavan/operators","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ortegavan%2Foperators","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ortegavan%2Foperators/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ortegavan%2Foperators/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ortegavan%2Foperators/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ortegavan","download_url":"https://codeload.github.com/ortegavan/operators/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ortegavan%2Foperators/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33742192,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-31T18:04:06.781Z","updated_at":"2026-05-31T18:04:06.833Z","avatar_url":"https://github.com/ortegavan.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Resource Operators\n\n\u003e Operadores de composição para a Resource API do Angular — inspirados no modelo mental do `pipe()` do RxJS.\n\n\u003cdiv align=\"center\"\u003e\n  \u003ca href=\"https://angular.dev\"\u003e\u003cimg src=\"https://img.shields.io/badge/Angular-21-DD0031?logo=angular\u0026logoColor=white\" alt=\"Angular\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.typescriptlang.org\"\u003e\u003cimg src=\"https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript\u0026logoColor=white\" alt=\"TypeScript\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://vitest.dev\"\u003e\u003cimg src=\"https://img.shields.io/badge/Vitest-4-6E9F18?logo=vitest\u0026logoColor=white\" alt=\"Vitest\" /\u003e\u003c/a\u003e\n\u003c/div\u003e\n\nRepositório de apoio a artigo em **https://medium.com/@ortegavan**.\n\n## Índice\n\n- [Sobre](#sobre)\n- [Operadores disponíveis](#operadores-disponíveis)\n- [Como rodar](#como-rodar)\n- [Uso](#uso)\n  - [withPreviousValue](#withpreviousvalue)\n  - [withRetry](#withretry)\n  - [withDebounce](#withdebounce)\n  - [withCache](#withcache)\n  - [withOptimisticUpdate](#withoptimisticupdate)\n- [Composição](#composição)\n- [Testes](#testes)\n- [Estrutura do projeto](#estrutura-do-projeto)\n\n---\n\n## Sobre\n\nO Angular 19+ introduziu a Resource API como forma declarativa de lidar com dados assíncronos. Com `resourceFromSnapshots` e `linkedSignal`, é possível **compor comportamentos em cima de qualquer Resource** de forma reutilizável — exatamente como fazíamos com operadores RxJS.\n\nCada operador é uma **função pura** que recebe `Resource\u003cT\u003e` e retorna `Resource\u003cT\u003e`. Isso permite encadeamento livre sem acoplamento com HTTP, Angular ou qualquer framework de estado.\n\n## Operadores disponíveis\n\n| Operador                                        | O que faz                                                             |\n| ----------------------------------------------- | --------------------------------------------------------------------- |\n| [`withPreviousValue`](#withpreviousvalue)       | Mantém o valor anterior durante o loading, evitando flash de conteúdo |\n| [`withRetry`](#withretry)                       | Retry automático com backoff exponencial em caso de erro              |\n| [`withDebounce`](#withdebounce)                 | Suprime o estado de loading na UI durante a janela de debounce        |\n| [`withCache`](#withcache)                       | Cache em memória com TTL configurável, compartilhado entre instâncias |\n| [`withOptimisticUpdate`](#withoptimisticupdate) | Atualiza a UI imediatamente antes da resposta do servidor             |\n\n---\n\n## Como rodar\n\n**Pré-requisitos:** Node.js 20+ e npm 10+\n\n```bash\nnpm install\n```\n\nEm dois terminais separados:\n\n```bash\n# Terminal 1 — API fake (porta 3000)\nnpm run api\n\n# Terminal 2 — Angular dev server (porta 4200)\nnpm start\n```\n\nAcesse `http://localhost:4200`.\n\n\u003e O dev server possui proxy configurado: chamadas para `/api/*` são redirecionadas automaticamente para `localhost:3000`.\n\n---\n\n## Uso\n\n### `withPreviousValue`\n\nGarante que `resource.value()` nunca fique `undefined` entre carregamentos — o valor anterior é mantido enquanto o novo chega.\n\n```typescript\nimport { withPreviousValue } from './operators/with-previous-value';\n\nreadonly user = withPreviousValue(\n  httpResource\u003cUser\u003e(() =\u003e `/api/users/${this.userId()}`)\n);\n// user.value() nunca é undefined entre navegações\n```\n\n---\n\n### `withRetry`\n\nTenta novamente automaticamente em caso de erro, com backoff exponencial.\n\n```typescript\nimport { withRetry } from './operators/with-retry';\n\nreadonly products = withRetry(\n  httpResource\u003cProduct[]\u003e(() =\u003e '/api/products'),\n  { maxRetries: 3, baseDelay: 500, maxDelay: 10000 }\n);\n```\n\n| Opção        | Padrão  | Descrição                                 |\n| ------------ | ------- | ----------------------------------------- |\n| `maxRetries` | `3`     | Número máximo de tentativas               |\n| `baseDelay`  | `1000`  | Delay inicial em ms                       |\n| `maxDelay`   | `10000` | Delay máximo (cap do backoff exponencial) |\n\n---\n\n### `withDebounce`\n\nSuprime o estado `loading` na UI durante a janela de debounce, evitando flicker em interações rápidas.\n\n```typescript\nimport { withDebounce } from './operators/with-debounce';\n\nreadonly results = withDebounce(\n  httpResource\u003cResult[]\u003e(() =\u003e `/api/search?q=${this.term()}`),\n  400 // delay em ms (padrão: 300)\n);\n```\n\n\u003e **Nota:** o debounce atua na **camada de apresentação** — ele suprime o flash de loading na UI. Para atrasar a própria requisição, aplique o delay no signal que alimenta a URL.\n\n---\n\n### `withCache`\n\nArmazena o resultado em memória e reutiliza enquanto o TTL não expirar. O cache é compartilhado globalmente entre todas as instâncias que usam a mesma `key`.\n\n```typescript\nimport { withCache } from './operators/with-cache';\n\nreadonly categories = withCache(\n  httpResource\u003cCategory[]\u003e(() =\u003e '/api/categories'),\n  { key: 'categories', ttl: 60000 }\n);\n```\n\n| Opção | Padrão      | Descrição             |\n| ----- | ----------- | --------------------- |\n| `key` | obrigatório | Chave global do cache |\n| `ttl` | `30000`     | Tempo de vida em ms   |\n\n\u003e Em testes, use `clearGlobalCache()` para isolar execuções entre casos de teste.\n\n---\n\n### `withOptimisticUpdate`\n\nAdiciona o método `applyOptimistic(value)` ao resource. A UI atualiza instantaneamente; se o servidor falhar, o estado é revertido automaticamente.\n\n```typescript\nimport { withOptimisticUpdate } from './operators/with-optimistic-update';\n\nreadonly todos = withOptimisticUpdate(\n  httpResource\u003cTodo[]\u003e(() =\u003e '/api/todos')\n);\n\nasync toggleTodo(todo: Todo): Promise\u003cvoid\u003e {\n  const updated = this.todos.value()!.map((t) =\u003e\n    t.id === todo.id ? { ...t, completed: !t.completed } : t\n  );\n\n  this.todos.applyOptimistic(updated);                          // UI atualiza imediatamente\n\n  await firstValueFrom(\n    this.http.patch(`/api/todos/${todo.id}`, { completed: !todo.completed })\n  );\n\n  this.todos.reload();                                          // Confirma com o servidor\n}\n```\n\n---\n\n## Composição\n\nOs operadores se encadeiam livremente, como no RxJS — leia de dentro para fora:\n\n```typescript\nreadonly user = withDebounce(\n  withRetry(\n    withPreviousValue(\n      httpResource\u003cUser\u003e(() =\u003e `/api/users/${this.userId()}`)\n    ),\n    { maxRetries: 3 }\n  ),\n  300\n);\n```\n\n| Camada              | Responsabilidade                                   |\n| ------------------- | -------------------------------------------------- |\n| `httpResource`      | Faz a requisição HTTP reativa                      |\n| `withPreviousValue` | Mantém o dado anterior durante loading             |\n| `withRetry`         | Tenta novamente em caso de falha (3x, com backoff) |\n| `withDebounce`      | Evita flash de loading em mudanças rápidas         |\n\n---\n\n## Testes\n\n```bash\nnpm test\n```\n\nOs operadores são testáveis de forma completamente isolada, sem HTTP real. O padrão é criar um `Resource` fake a partir de um `signal` de snapshots:\n\n```typescript\nimport { TestBed } from '@angular/core/testing';\nimport { resourceFromSnapshots, ResourceSnapshot, signal } from '@angular/core';\nimport { withPreviousValue } from './with-previous-value';\n\nit('deve manter o valor anterior durante loading', () =\u003e {\n  let snapshot!: WritableSignal\u003cResourceSnapshot\u003cstring | undefined\u003e\u003e;\n  let composed!: Resource\u003cstring | undefined\u003e;\n\n  TestBed.runInInjectionContext(() =\u003e {\n    snapshot = signal\u003cResourceSnapshot\u003cstring | undefined\u003e\u003e({\n      status: 'resolved',\n      value: 'dados originais',\n    });\n\n    composed = withPreviousValue(resourceFromSnapshots(snapshot));\n  });\n\n  snapshot.set({ status: 'loading', value: undefined });\n  TestBed.flushEffects();\n\n  expect(composed.value()).toBe('dados originais'); // sem flash!\n});\n```\n\nSem HTTP, sem servidor, sem timer — apenas a transformação do snapshot.\n\n---\n\n## Estrutura do projeto\n\n```\nsrc/\n└── app/\n    ├── model/\n    │   ├── user.ts\n    │   ├── product.ts\n    │   └── todo.ts\n    ├── operators/\n    │   ├── with-previous-value.ts\n    │   ├── with-previous-value.spec.ts\n    │   ├── with-retry.ts\n    │   ├── with-retry.spec.ts\n    │   ├── with-debounce.ts\n    │   ├── with-debounce.spec.ts\n    │   ├── with-cache.ts\n    │   ├── with-cache.spec.ts\n    │   ├── with-optimistic-update.ts\n    │   └── with-optimistic-update.spec.ts\n    ├── services/\n    │   ├── user.service.ts\n    │   ├── product.service.ts\n    │   └── todo.service.ts\n    ├── app.ts\n    ├── app.html\n    └── app.config.ts\ndb.json                 # Dados da API fake (json-server)\nproxy.conf.json         # Proxy /api → localhost:3000\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fortegavan%2Foperators","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fortegavan%2Foperators","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fortegavan%2Foperators/lists"}