@yaebal/media-cache 0.0.1 → 0.0.3

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.
package/README.md CHANGED
@@ -1,11 +1,48 @@
1
1
  # @yaebal/media-cache
2
2
 
3
- where to keep `key → file_id`. defaults to in-memory (lost on restart).
3
+ reuse a `file_id` instead of re-uploading the same file.
4
4
 
5
- ## install
5
+ ## manual mode
6
6
 
7
- ```sh
8
- pnpm add @yaebal/media-cache
7
+ ```ts
8
+ import { mediaCache } from "@yaebal/media-cache"
9
+ import { media } from "yaebal"
10
+
11
+ const cache = mediaCache()
12
+
13
+ bot.command("logo", async (ctx) => {
14
+ await cache.photo(ctx, "logo", media.path("./logo.png"))
15
+ })
16
+ ```
17
+
18
+ ## transparent mode (hook into `ctx.send*`)
19
+
20
+ ```ts
21
+ import { mediaCache } from "@yaebal/media-cache"
22
+ import { media } from "yaebal"
23
+
24
+ const cache = mediaCache()
25
+ cache.hook(bot.api)
26
+
27
+ // all sendPhoto/sendDocument/sendAudio/... auto-cache by path or URL
28
+ bot.command("logo", async (ctx) => {
29
+ await ctx.sendPhoto(media.path("./logo.png")) // first time: upload
30
+ await ctx.sendPhoto(media.path("./logo.png")) // second time: cached file_id
31
+ })
32
+ ```
33
+
34
+ keyed by the source path or URL — if you send the same path twice, it reuses the `file_id` from the first response.
35
+
36
+ ## storage
37
+
38
+ defaults to in-memory (`MemoryStorage`, lost on restart). pass a persistent storage:
39
+
40
+ ```ts
41
+ import { mediaCache } from "@yaebal/media-cache"
42
+
43
+ const cache = mediaCache({
44
+ storage: myRedisStorage, // any StorageAdapter<string>
45
+ })
9
46
  ```
10
47
 
11
48
  ---
package/lib/index.d.ts CHANGED
@@ -1,20 +1,32 @@
1
- import { type Context, type MediaSource, type Message } from "@yaebal/core";
1
+ import { type Api, type Context, type MediaSource, type Message } from "@yaebal/core";
2
2
  import { type StorageAdapter } from "@yaebal/session";
3
3
  export interface MediaCacheOptions {
4
- /** where to keep `key → file_id`. defaults to in-memory (lost on restart). */
5
4
  storage?: StorageAdapter<string>;
6
5
  }
7
6
  export interface MediaCache {
8
- /** send a photo, caching its `file_id` under `key` to skip future uploads. */
9
7
  photo(ctx: Context, key: string, source: MediaSource | string, extra?: Record<string, unknown>): Promise<Message>;
10
- /** send a document, caching its `file_id` under `key`. */
11
8
  document(ctx: Context, key: string, source: MediaSource | string, extra?: Record<string, unknown>): Promise<Message>;
9
+ hook(api: Api): void;
10
+ /**
11
+ * cached media sources — `.path()` and `.url()` return thenables that
12
+ * transparently resolve to a cached `file_id` string (if available) or
13
+ * the original source. works directly in `ctx.send*()` — no wrapper needed.
14
+ *
15
+ * @example
16
+ * const cache = mediaCache()
17
+ * bot.command("logo", async (ctx) => {
18
+ * await ctx.sendPhoto(cache.media.path("./logo.png"))
19
+ * })
20
+ */
21
+ media: CachedMedia;
22
+ }
23
+ export interface CachedMedia {
24
+ path(p: string): MediaSource & {
25
+ then(resolve: (value: string | MediaSource) => unknown): unknown;
26
+ };
27
+ url(u: string): MediaSource & {
28
+ then(resolve: (value: string | MediaSource) => unknown): unknown;
29
+ };
12
30
  }
13
- /**
14
- * the first time you send a local file under a `key`, telegram returns a
15
- * `file_id`; this caches it and reuses it on later sends — no re-upload.
16
- * caller-supplied keys, so it's correct under concurrency (unlike a transparent
17
- * hook that has to guess which upload produced which id).
18
- */
19
31
  export declare function mediaCache(options?: MediaCacheOptions): MediaCache;
20
32
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,OAAO,EAAS,MAAM,cAAc,CAAC;AACnF,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAErE,MAAM,WAAW,iBAAiB;IACjC,8EAA8E;IAC9E,OAAO,CAAC,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IAC1B,8EAA8E;IAC9E,KAAK,CACJ,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,GAAG,MAAM,EAC5B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,0DAA0D;IAC1D,QAAQ,CACP,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,GAAG,MAAM,EAC5B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,OAAO,CAAC,CAAC;CACpB;AASD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU,CA6BtE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,GAAG,EACR,KAAK,OAAO,EAEZ,KAAK,WAAW,EAChB,KAAK,OAAO,EAEZ,MAAM,cAAc,CAAC;AACtB,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAErE,MAAM,WAAW,iBAAiB;IACjC,OAAO,CAAC,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IAC1B,KAAK,CACJ,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,GAAG,MAAM,EAC5B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,QAAQ,CACP,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,GAAG,MAAM,EAC5B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC;IACrB;;;;;;;;;;OAUG;IACH,KAAK,EAAE,WAAW,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC3B,IAAI,CACH,CAAC,EAAE,MAAM,GACP,WAAW,GAAG;QAAE,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,KAAK,OAAO,GAAG,OAAO,CAAA;KAAE,CAAC;IACtF,GAAG,CACF,CAAC,EAAE,MAAM,GACP,WAAW,GAAG;QAAE,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,KAAK,OAAO,GAAG,OAAO,CAAA;KAAE,CAAC;CACtF;AA0CD,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU,CAwEtE"}
package/lib/index.js CHANGED
@@ -1,19 +1,43 @@
1
- import { media } from "@yaebal/core";
1
+ import { isMediaSource, media, } from "@yaebal/core";
2
2
  import { MemoryStorage } from "@yaebal/session";
3
3
  function extractFileId(result, field) {
4
4
  const value = result[field];
5
- const node = Array.isArray(value) ? value[value.length - 1] : value;
5
+ const node = Array.isArray(value) ? value.at(-1) : value;
6
6
  return node?.file_id;
7
7
  }
8
- /**
9
- * the first time you send a local file under a `key`, telegram returns a
10
- * `file_id`; this caches it and reuses it on later sends — no re-upload.
11
- * caller-supplied keys, so it's correct under concurrency (unlike a transparent
12
- * hook that has to guess which upload produced which id).
13
- */
8
+ const MEDIA_METHODS = {
9
+ sendPhoto: { param: "photo", field: "photo" },
10
+ sendDocument: { param: "document", field: "document" },
11
+ sendAudio: { param: "audio", field: "audio" },
12
+ sendVideo: { param: "video", field: "video" },
13
+ sendAnimation: { param: "animation", field: "animation" },
14
+ sendVoice: { param: "voice", field: "voice" },
15
+ sendVideoNote: { param: "video_note", field: "video_note" },
16
+ sendSticker: { param: "sticker", field: "sticker" },
17
+ };
18
+ function computeKey(source) {
19
+ if (source.kind === "path")
20
+ return `path:${source.path}`;
21
+ if (source.kind === "url")
22
+ return `url:${source.url}`;
23
+ return undefined;
24
+ }
25
+ function createCachedSource(source, storage_) {
26
+ const key = computeKey(source);
27
+ return {
28
+ ...source,
29
+ then(resolve) {
30
+ if (!key)
31
+ return resolve(source);
32
+ return Promise.resolve(storage_.get(key)).then((cached) => {
33
+ resolve(cached ? media.fileId(cached) : source);
34
+ });
35
+ },
36
+ };
37
+ }
14
38
  export function mediaCache(options = {}) {
15
39
  const storage = options.storage ?? new MemoryStorage();
16
- const send = async (ctx, key, source, field, sender, extra) => {
40
+ const send = async (_ctx, key, source, field, sender, extra) => {
17
41
  const cached = await storage.get(key);
18
42
  const result = await sender(cached ? media.fileId(cached) : source, extra);
19
43
  if (!cached) {
@@ -26,6 +50,45 @@ export function mediaCache(options = {}) {
26
50
  return {
27
51
  photo: (ctx, key, source, extra) => send(ctx, key, source, "photo", (s, e) => ctx.sendPhoto(s, e), extra),
28
52
  document: (ctx, key, source, extra) => send(ctx, key, source, "document", (s, e) => ctx.sendDocument(s, e), extra),
53
+ media: {
54
+ path: (p) => createCachedSource(media.path(p), storage),
55
+ url: (u) => createCachedSource(media.url(u), storage),
56
+ },
57
+ hook(api) {
58
+ const cachedBefore = new Map();
59
+ api.before(async (method, params) => {
60
+ const config = MEDIA_METHODS[method];
61
+ if (!config || !params)
62
+ return;
63
+ const src = params[config.param];
64
+ if (!isMediaSource(src))
65
+ return;
66
+ const key = computeKey(src);
67
+ if (!key)
68
+ return;
69
+ const fileId = await storage.get(key);
70
+ if (fileId) {
71
+ params[config.param] = fileId;
72
+ return params;
73
+ }
74
+ cachedBefore.set(params, key);
75
+ return params;
76
+ });
77
+ api.after(async (method, params, result) => {
78
+ if (!params)
79
+ return;
80
+ const config = MEDIA_METHODS[method];
81
+ if (!config)
82
+ return;
83
+ const key = cachedBefore.get(params);
84
+ if (!key)
85
+ return;
86
+ cachedBefore.delete(params);
87
+ const id = extractFileId(result, config.field);
88
+ if (id)
89
+ await storage.set(key, id);
90
+ });
91
+ },
29
92
  };
30
93
  }
31
94
  //# sourceMappingURL=index.js.map
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgD,KAAK,EAAE,MAAM,cAAc,CAAC;AACnF,OAAO,EAAE,aAAa,EAAuB,MAAM,iBAAiB,CAAC;AAwBrE,SAAS,aAAa,CAAC,MAAe,EAAE,KAAa;IACpD,MAAM,KAAK,GAAI,MAA6C,CAAC,KAAK,CAAC,CAAC;IACpE,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAEpE,OAAQ,IAAyC,EAAE,OAAO,CAAC;AAC5D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,UAA6B,EAAE;IACzD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,IAAI,aAAa,EAAU,CAAC;IAE/D,MAAM,IAAI,GAAG,KAAK,EACjB,GAAY,EACZ,GAAW,EACX,MAA4B,EAC5B,KAA2B,EAC3B,MAAwF,EACxF,KAA+B,EACZ,EAAE;QACrB,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACtC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAE3E,IAAI,CAAC,MAAM,EAAE,CAAC;YACb,MAAM,EAAE,GAAG,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;YAExC,IAAI,EAAE;gBAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACpC,CAAC;QAED,OAAO,MAAM,CAAC;IACf,CAAC,CAAC;IAEF,OAAO;QACN,KAAK,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAClC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC;QACtE,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CACrC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC;KAC5E,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaebal/media-cache",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "yaebal media-cache — reuse a file_id instead of re-uploading the same file.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -16,8 +16,8 @@
16
16
  "src"
17
17
  ],
18
18
  "dependencies": {
19
- "@yaebal/core": "0.0.1",
20
- "@yaebal/session": "0.0.1"
19
+ "@yaebal/core": "0.0.5",
20
+ "@yaebal/session": "0.0.2"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "latest"
package/src/index.test.ts CHANGED
@@ -50,7 +50,7 @@ test("distinct keys cache independently", async () => {
50
50
  const cache = mediaCache();
51
51
  await cache.photo(ctx, "a", media.path("./a.png"));
52
52
  await cache.photo(ctx, "b", media.path("./b.png")); // different key → uploads
53
-
53
+
54
54
  assert.equal(sent[0]?.kind, "path");
55
55
  assert.equal(sent[1]?.kind, "path");
56
56
  });
package/src/index.ts CHANGED
@@ -1,46 +1,99 @@
1
- import { type Context, type MediaSource, type Message, media } from "@yaebal/core";
1
+ import {
2
+ type Api,
3
+ type Context,
4
+ isMediaSource,
5
+ type MediaSource,
6
+ type Message,
7
+ media,
8
+ } from "@yaebal/core";
2
9
  import { MemoryStorage, type StorageAdapter } from "@yaebal/session";
3
10
 
4
11
  export interface MediaCacheOptions {
5
- /** where to keep `key → file_id`. defaults to in-memory (lost on restart). */
6
12
  storage?: StorageAdapter<string>;
7
13
  }
8
14
 
9
15
  export interface MediaCache {
10
- /** send a photo, caching its `file_id` under `key` to skip future uploads. */
11
16
  photo(
12
17
  ctx: Context,
13
18
  key: string,
14
19
  source: MediaSource | string,
15
20
  extra?: Record<string, unknown>,
16
21
  ): Promise<Message>;
17
- /** send a document, caching its `file_id` under `key`. */
18
22
  document(
19
23
  ctx: Context,
20
24
  key: string,
21
25
  source: MediaSource | string,
22
26
  extra?: Record<string, unknown>,
23
27
  ): Promise<Message>;
28
+ hook(api: Api): void;
29
+ /**
30
+ * cached media sources — `.path()` and `.url()` return thenables that
31
+ * transparently resolve to a cached `file_id` string (if available) or
32
+ * the original source. works directly in `ctx.send*()` — no wrapper needed.
33
+ *
34
+ * @example
35
+ * const cache = mediaCache()
36
+ * bot.command("logo", async (ctx) => {
37
+ * await ctx.sendPhoto(cache.media.path("./logo.png"))
38
+ * })
39
+ */
40
+ media: CachedMedia;
41
+ }
42
+
43
+ export interface CachedMedia {
44
+ path(
45
+ p: string,
46
+ ): MediaSource & { then(resolve: (value: string | MediaSource) => unknown): unknown };
47
+ url(
48
+ u: string,
49
+ ): MediaSource & { then(resolve: (value: string | MediaSource) => unknown): unknown };
24
50
  }
25
51
 
26
52
  function extractFileId(result: Message, field: string): string | undefined {
27
53
  const value = (result as unknown as Record<string, unknown>)[field];
28
- const node = Array.isArray(value) ? value[value.length - 1] : value;
29
-
54
+ const node = Array.isArray(value) ? value.at(-1) : value;
30
55
  return (node as { file_id?: string } | undefined)?.file_id;
31
56
  }
32
57
 
33
- /**
34
- * the first time you send a local file under a `key`, telegram returns a
35
- * `file_id`; this caches it and reuses it on later sends — no re-upload.
36
- * caller-supplied keys, so it's correct under concurrency (unlike a transparent
37
- * hook that has to guess which upload produced which id).
38
- */
58
+ const MEDIA_METHODS: Record<string, { param: string; field: string }> = {
59
+ sendPhoto: { param: "photo", field: "photo" },
60
+ sendDocument: { param: "document", field: "document" },
61
+ sendAudio: { param: "audio", field: "audio" },
62
+ sendVideo: { param: "video", field: "video" },
63
+ sendAnimation: { param: "animation", field: "animation" },
64
+ sendVoice: { param: "voice", field: "voice" },
65
+ sendVideoNote: { param: "video_note", field: "video_note" },
66
+ sendSticker: { param: "sticker", field: "sticker" },
67
+ };
68
+
69
+ function computeKey(source: MediaSource): string | undefined {
70
+ if (source.kind === "path") return `path:${source.path}`;
71
+ if (source.kind === "url") return `url:${source.url}`;
72
+ return undefined;
73
+ }
74
+
75
+ function createCachedSource(
76
+ source: MediaSource,
77
+ storage_: StorageAdapter<string>,
78
+ ): MediaSource & { then(resolve: (value: string | MediaSource) => unknown): unknown } {
79
+ const key = computeKey(source);
80
+
81
+ return {
82
+ ...source,
83
+ then(resolve: (value: string | MediaSource) => unknown) {
84
+ if (!key) return resolve(source);
85
+ return Promise.resolve(storage_.get(key)).then((cached) => {
86
+ resolve(cached ? media.fileId(cached) : source);
87
+ });
88
+ },
89
+ };
90
+ }
91
+
39
92
  export function mediaCache(options: MediaCacheOptions = {}): MediaCache {
40
93
  const storage = options.storage ?? new MemoryStorage<string>();
41
94
 
42
95
  const send = async (
43
- ctx: Context,
96
+ _ctx: Context,
44
97
  key: string,
45
98
  source: MediaSource | string,
46
99
  field: "photo" | "document",
@@ -52,7 +105,7 @@ export function mediaCache(options: MediaCacheOptions = {}): MediaCache {
52
105
 
53
106
  if (!cached) {
54
107
  const id = extractFileId(result, field);
55
-
108
+
56
109
  if (id) await storage.set(key, id);
57
110
  }
58
111
 
@@ -64,5 +117,48 @@ export function mediaCache(options: MediaCacheOptions = {}): MediaCache {
64
117
  send(ctx, key, source, "photo", (s, e) => ctx.sendPhoto(s, e), extra),
65
118
  document: (ctx, key, source, extra) =>
66
119
  send(ctx, key, source, "document", (s, e) => ctx.sendDocument(s, e), extra),
120
+ media: {
121
+ path: (p: string) => createCachedSource(media.path(p), storage),
122
+ url: (u: string) => createCachedSource(media.url(u), storage),
123
+ },
124
+ hook(api: Api) {
125
+ const cachedBefore = new Map<object, string>();
126
+
127
+ api.before(async (method, params) => {
128
+ const config = MEDIA_METHODS[method];
129
+ if (!config || !params) return;
130
+
131
+ const src = params[config.param];
132
+ if (!isMediaSource(src)) return;
133
+
134
+ const key = computeKey(src);
135
+ if (!key) return;
136
+
137
+ const fileId = await storage.get(key);
138
+ if (fileId) {
139
+ params[config.param] = fileId;
140
+ return params;
141
+ }
142
+
143
+ cachedBefore.set(params, key);
144
+
145
+ return params;
146
+ });
147
+
148
+ api.after(async (method, params, result) => {
149
+ if (!params) return;
150
+
151
+ const config = MEDIA_METHODS[method];
152
+ if (!config) return;
153
+
154
+ const key = cachedBefore.get(params);
155
+ if (!key) return;
156
+
157
+ cachedBefore.delete(params);
158
+
159
+ const id = extractFileId(result as Message, config.field);
160
+ if (id) await storage.set(key, id);
161
+ });
162
+ },
67
163
  };
68
164
  }