envio 3.3.0-alpha.12 → 3.3.0-alpha.13

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.
@@ -606,29 +606,39 @@ let effectLabelsSchema = S.object(s => {
606
606
  s.field("effect", S.string)
607
607
  })
608
608
 
609
+ // For metrics whose backing state lives per scope ("crossChain" or a chain
610
+ // id) — without the label, scopes of the same effect would clobber each
611
+ // other's gauge value.
612
+ let effectScopeLabelsSchema = S.schema(s =>
613
+ {
614
+ "effect": s.matches(S.string),
615
+ "scope": s.matches(S.string),
616
+ }
617
+ )
618
+
609
619
  module EffectCalls = {
610
620
  let timeCounter = SafeCounter.makeOrThrow(
611
621
  ~name="envio_effect_call_seconds",
612
622
  ~help="Processing time taken to call the Effect function.",
613
- ~labelSchema=effectLabelsSchema,
623
+ ~labelSchema=effectScopeLabelsSchema,
614
624
  )
615
625
 
616
626
  let sumTimeCounter = SafeCounter.makeOrThrow(
617
627
  ~name="envio_effect_call_seconds_total",
618
628
  ~help="Cumulative time spent calling the Effect function during the indexing process.",
619
- ~labelSchema=effectLabelsSchema,
629
+ ~labelSchema=effectScopeLabelsSchema,
620
630
  )
621
631
 
622
632
  let totalCallsCount = SafeCounter.makeOrThrow(
623
633
  ~name="envio_effect_call_total",
624
634
  ~help="Cumulative number of resolved Effect function calls during the indexing process.",
625
- ~labelSchema=effectLabelsSchema,
635
+ ~labelSchema=effectScopeLabelsSchema,
626
636
  )
627
637
 
628
638
  let activeCallsCount = SafeGauge.makeOrThrow(
629
639
  ~name="envio_effect_active_calls",
630
640
  ~help="The number of Effect function calls that are currently running.",
631
- ~labelSchema=effectLabelsSchema,
641
+ ~labelSchema=effectScopeLabelsSchema,
632
642
  )
633
643
  }
634
644
 
@@ -636,11 +646,11 @@ module EffectCacheCount = {
636
646
  let gauge = SafeGauge.makeOrThrow(
637
647
  ~name="envio_effect_cache",
638
648
  ~help="The number of items in the effect cache.",
639
- ~labelSchema=effectLabelsSchema,
649
+ ~labelSchema=effectScopeLabelsSchema,
640
650
  )
641
651
 
642
- let set = (~count, ~effectName) => {
643
- gauge->SafeGauge.handleInt(~labels=effectName, ~value=count)
652
+ let set = (~count, ~effectName, ~scope) => {
653
+ gauge->SafeGauge.handleInt(~labels={"effect": effectName, "scope": scope}, ~value=count)
644
654
  }
645
655
  }
646
656
 
@@ -660,7 +670,7 @@ module EffectQueueCount = {
660
670
  let gauge = SafeGauge.makeOrThrow(
661
671
  ~name="envio_effect_queue",
662
672
  ~help="The number of effect calls waiting in the rate limit queue.",
663
- ~labelSchema=effectLabelsSchema,
673
+ ~labelSchema=effectScopeLabelsSchema,
664
674
  )
665
675
 
666
676
  let timeCounter = SafeCounter.makeOrThrow(
@@ -669,8 +679,8 @@ module EffectQueueCount = {
669
679
  ~labelSchema=effectLabelsSchema,
670
680
  )
671
681
 
672
- let set = (~count, ~effectName) => {
673
- gauge->SafeGauge.handleInt(~labels=effectName, ~value=count)
682
+ let set = (~count, ~effectName, ~scope) => {
683
+ gauge->SafeGauge.handleInt(~labels={"effect": effectName, "scope": scope}, ~value=count)
674
684
  }
675
685
  }
676
686
 
@@ -651,13 +651,18 @@ let ProgressLatency = {
651
651
 
652
652
  let effectLabelsSchema = S$RescriptSchema.object(s => s.f("effect", S$RescriptSchema.string));
653
653
 
654
- let timeCounter$5 = makeOrThrow("envio_effect_call_seconds", "Processing time taken to call the Effect function.", effectLabelsSchema);
654
+ let effectScopeLabelsSchema = S$RescriptSchema.schema(s => ({
655
+ effect: s.m(S$RescriptSchema.string),
656
+ scope: s.m(S$RescriptSchema.string)
657
+ }));
658
+
659
+ let timeCounter$5 = makeOrThrow("envio_effect_call_seconds", "Processing time taken to call the Effect function.", effectScopeLabelsSchema);
655
660
 
656
- let sumTimeCounter$1 = makeOrThrow("envio_effect_call_seconds_total", "Cumulative time spent calling the Effect function during the indexing process.", effectLabelsSchema);
661
+ let sumTimeCounter$1 = makeOrThrow("envio_effect_call_seconds_total", "Cumulative time spent calling the Effect function during the indexing process.", effectScopeLabelsSchema);
657
662
 
658
- let totalCallsCount = makeOrThrow("envio_effect_call_total", "Cumulative number of resolved Effect function calls during the indexing process.", effectLabelsSchema);
663
+ let totalCallsCount = makeOrThrow("envio_effect_call_total", "Cumulative number of resolved Effect function calls during the indexing process.", effectScopeLabelsSchema);
659
664
 
660
- let activeCallsCount = makeOrThrow$1("envio_effect_active_calls", "The number of Effect function calls that are currently running.", effectLabelsSchema);
665
+ let activeCallsCount = makeOrThrow$1("envio_effect_active_calls", "The number of Effect function calls that are currently running.", effectScopeLabelsSchema);
661
666
 
662
667
  let EffectCalls = {
663
668
  timeCounter: timeCounter$5,
@@ -666,10 +671,13 @@ let EffectCalls = {
666
671
  activeCallsCount: activeCallsCount
667
672
  };
668
673
 
669
- let gauge$19 = makeOrThrow$1("envio_effect_cache", "The number of items in the effect cache.", effectLabelsSchema);
674
+ let gauge$19 = makeOrThrow$1("envio_effect_cache", "The number of items in the effect cache.", effectScopeLabelsSchema);
670
675
 
671
- function set$19(count, effectName) {
672
- handleInt$1(gauge$19, effectName, count);
676
+ function set$19(count, effectName, scope) {
677
+ handleInt$1(gauge$19, {
678
+ effect: effectName,
679
+ scope: scope
680
+ }, count);
673
681
  }
674
682
 
675
683
  let EffectCacheCount = {
@@ -688,12 +696,15 @@ let EffectCacheInvalidationsCount = {
688
696
  increment: increment$7
689
697
  };
690
698
 
691
- let gauge$20 = makeOrThrow$1("envio_effect_queue", "The number of effect calls waiting in the rate limit queue.", effectLabelsSchema);
699
+ let gauge$20 = makeOrThrow$1("envio_effect_queue", "The number of effect calls waiting in the rate limit queue.", effectScopeLabelsSchema);
692
700
 
693
701
  let timeCounter$6 = makeOrThrow("envio_effect_queue_wait_seconds", "The time spent waiting in the rate limit queue.", effectLabelsSchema);
694
702
 
695
- function set$20(count, effectName) {
696
- handleInt$1(gauge$20, effectName, count);
703
+ function set$20(count, effectName, scope) {
704
+ handleInt$1(gauge$20, {
705
+ effect: effectName,
706
+ scope: scope
707
+ }, count);
697
708
  }
698
709
 
699
710
  let EffectQueueCount = {
@@ -825,6 +836,7 @@ export {
825
836
  ProgressEventsCount,
826
837
  ProgressLatency,
827
838
  effectLabelsSchema,
839
+ effectScopeLabelsSchema,
828
840
  EffectCalls,
829
841
  EffectCacheCount,
830
842
  EffectCacheInvalidationsCount,
@@ -28,10 +28,23 @@ Utils.Object.defineProperty(
28
28
  },
29
29
  )
30
30
  %%raw(`
31
- var EffectContext = function(params, defaultShouldCache, callEffect) {
31
+ // Top-level so the getter is created once, not per context. \`this\` is the
32
+ // EffectContext instance; only cross-chain contexts install it.
33
+ function throwCrossChainChainAccess() {
34
+ throw new Error('context.chain is not available on the cross-chain effect "' + this._effectName + '". Set \`crossChain: false\` in its options to scope the effect to a single chain, then read context.chain.id.');
35
+ }
36
+ var crossChainChainDescriptor = { get: throwCrossChainChainAccess, enumerable: true };
37
+ var EffectContext = function(params, chainId, effectName, defaultShouldCache, callEffect) {
32
38
  paramsByThis.set(this, params);
33
39
  this.effect = callEffect;
34
40
  this.cache = defaultShouldCache;
41
+ if (chainId === undefined) {
42
+ // Cross-chain: reading context.chain throws, via the shared getter.
43
+ Object.defineProperty(this, "_effectName", { value: effectName });
44
+ Object.defineProperty(this, "chain", crossChainChainDescriptor);
45
+ } else {
46
+ this.chain = { id: chainId };
47
+ }
35
48
  };
36
49
  EffectContext.prototype = effectContextPrototype;
37
50
  `)
@@ -39,35 +52,61 @@ EffectContext.prototype = effectContextPrototype;
39
52
  @new
40
53
  external makeEffectContext: (
41
54
  contextParams,
55
+ ~chainId: option<int>,
56
+ ~effectName: string,
42
57
  ~defaultShouldCache: bool,
43
58
  ~callEffect: (Internal.effect, Internal.effectInput) => promise<Internal.effectOutput>,
44
59
  ) => Internal.effectContext = "EffectContext"
45
60
 
46
61
  let initEffect = (params: contextParams) => {
47
- let rec callEffect = (effect: Internal.effect, input: Internal.effectInput) => {
48
- let effectContext = makeEffectContext(
49
- params,
50
- ~defaultShouldCache=effect.defaultShouldCache,
51
- ~callEffect,
52
- )
53
- let effectArgs: Internal.effectArgs = {
54
- input,
55
- context: effectContext,
56
- cacheKey: input->S.reverseConvertOrThrow(effect.input)->Utils.Hash.makeOrThrow,
57
- checkpointId: params.checkpointId,
62
+ let handlerChainId = params.item->Internal.getItemChainId
63
+ // A chain-scoped effect always resolves against the chain of the handler that
64
+ // triggered the call, even several effects deep, so the chain id is captured
65
+ // once from the item and reused for the whole nested-call tree.
66
+ let rec makeCaller = (~caller: option<Internal.effect>) => {
67
+ (effect: Internal.effect, input: Internal.effectInput) => {
68
+ let scope = effect.crossChain ? Internal.CrossChain : Internal.Chain(handlerChainId)
69
+
70
+ switch caller {
71
+ | Some(callerEffect) if callerEffect.crossChain && !effect.crossChain =>
72
+ // A cross-chain effect isn't tied to a single chain, so it has no chain
73
+ // to resolve a chain-scoped child against. Reject before any cache work.
74
+ JsError.throwWithMessage(
75
+ `The cross-chain effect "${callerEffect.name}" cannot call the chain-scoped effect "${effect.name}", because a cross-chain effect isn't tied to a single chain. Make "${effect.name}" cross-chain (\`crossChain: true\`), or make "${callerEffect.name}" chain-scoped (\`crossChain: false\`).`,
76
+ )
77
+ | _ => ()
78
+ }
79
+
80
+ let effectContext = makeEffectContext(
81
+ params,
82
+ ~chainId=switch scope {
83
+ | Internal.Chain(chainId) => Some(chainId)
84
+ | Internal.CrossChain => None
85
+ },
86
+ ~effectName=effect.name,
87
+ ~defaultShouldCache=effect.defaultShouldCache,
88
+ ~callEffect=makeCaller(~caller=Some(effect)),
89
+ )
90
+ let effectArgs: Internal.effectArgs = {
91
+ input,
92
+ context: effectContext,
93
+ cacheKey: input->S.reverseConvertOrThrow(effect.input)->Utils.Hash.makeOrThrow,
94
+ checkpointId: params.checkpointId,
95
+ }
96
+ LoadLayer.loadEffect(
97
+ ~loadManager=params.loadManager,
98
+ ~persistence=params.persistence,
99
+ ~effect,
100
+ ~effectArgs,
101
+ ~scope,
102
+ ~indexerState=params.indexerState,
103
+ ~shouldGroup=params.isPreload,
104
+ ~item=params.item,
105
+ ~ecosystem=params.config.ecosystem,
106
+ )
58
107
  }
59
- LoadLayer.loadEffect(
60
- ~loadManager=params.loadManager,
61
- ~persistence=params.persistence,
62
- ~effect,
63
- ~effectArgs,
64
- ~indexerState=params.indexerState,
65
- ~shouldGroup=params.isPreload,
66
- ~item=params.item,
67
- ~ecosystem=params.config.ecosystem,
68
- )
69
108
  }
70
- callEffect
109
+ makeCaller(~caller=None)
71
110
  }
72
111
 
73
112
  type entityContextParams = {
@@ -25,17 +25,37 @@ Object.defineProperty(effectContextPrototype, "log", {
25
25
  }
26
26
  });
27
27
 
28
- var EffectContext = function(params, defaultShouldCache, callEffect) {
28
+ // Top-level so the getter is created once, not per context. \`this\` is the
29
+ // EffectContext instance; only cross-chain contexts install it.
30
+ function throwCrossChainChainAccess() {
31
+ throw new Error('context.chain is not available on the cross-chain effect "' + this._effectName + '". Set \`crossChain: false\` in its options to scope the effect to a single chain, then read context.chain.id.');
32
+ }
33
+ var crossChainChainDescriptor = { get: throwCrossChainChainAccess, enumerable: true };
34
+ var EffectContext = function(params, chainId, effectName, defaultShouldCache, callEffect) {
29
35
  paramsByThis.set(this, params);
30
36
  this.effect = callEffect;
31
37
  this.cache = defaultShouldCache;
38
+ if (chainId === undefined) {
39
+ // Cross-chain: reading context.chain throws, via the shared getter.
40
+ Object.defineProperty(this, "_effectName", { value: effectName });
41
+ Object.defineProperty(this, "chain", crossChainChainDescriptor);
42
+ } else {
43
+ this.chain = { id: chainId };
44
+ }
32
45
  };
33
46
  EffectContext.prototype = effectContextPrototype;
34
47
  ;
35
48
 
36
49
  function initEffect(params) {
37
- let callEffect = (effect, input) => {
38
- let effectContext = new EffectContext(params, effect.defaultShouldCache, callEffect);
50
+ let handlerChainId = Internal.getItemChainId(params.item);
51
+ let makeCaller = caller => ((effect, input) => {
52
+ let scope = effect.crossChain ? "crossChain" : handlerChainId;
53
+ if (caller !== undefined && caller.crossChain && !effect.crossChain) {
54
+ Stdlib_JsError.throwWithMessage(`The cross-chain effect "` + caller.name + `" cannot call the chain-scoped effect "` + effect.name + `", because a cross-chain effect isn't tied to a single chain. Make "` + effect.name + `" cross-chain (\`crossChain: true\`), or make "` + caller.name + `" chain-scoped (\`crossChain: false\`).`);
55
+ }
56
+ let tmp;
57
+ tmp = scope === "crossChain" ? undefined : scope;
58
+ let effectContext = new EffectContext(params, tmp, effect.name, effect.defaultShouldCache, makeCaller(effect));
39
59
  let effectArgs_cacheKey = Utils.Hash.makeOrThrow(S$RescriptSchema.reverseConvertOrThrow(input, effect.input));
40
60
  let effectArgs_checkpointId = params.checkpointId;
41
61
  let effectArgs = {
@@ -44,9 +64,9 @@ function initEffect(params) {
44
64
  cacheKey: effectArgs_cacheKey,
45
65
  checkpointId: effectArgs_checkpointId
46
66
  };
47
- return LoadLayer.loadEffect(params.loadManager, params.persistence, effect, effectArgs, params.indexerState, params.isPreload, params.item, params.config.ecosystem);
48
- };
49
- return callEffect;
67
+ return LoadLayer.loadEffect(params.loadManager, params.persistence, effect, effectArgs, scope, params.indexerState, params.isPreload, params.item, params.config.ecosystem);
68
+ });
69
+ return makeCaller(undefined);
50
70
  }
51
71
 
52
72
  function getWhereHandler(params, filter) {
package/src/Writing.res CHANGED
@@ -16,8 +16,8 @@ let getChangesCount = (state: IndexerState.t) => {
16
16
  total := total.contents +. (state->InMemoryStore.getInMemTable(~entityConfig)).changesCount
17
17
  })
18
18
  state
19
- ->IndexerState.effects
20
- ->Utils.Dict.forEach(inMemTable => {
19
+ ->IndexerState.effectState
20
+ ->EffectState.forEach(inMemTable => {
21
21
  total := total.contents +. inMemTable.changesCount
22
22
  })
23
23
  state
@@ -38,9 +38,9 @@ let waitForCommit = (state: IndexerState.t): promise<unit> =>
38
38
  let snapshotEffects = (state: IndexerState.t, ~cache): array<Persistence.updatedEffectCache> => {
39
39
  let acc = []
40
40
  state
41
- ->IndexerState.effects
42
- ->Utils.Dict.forEach(inMemTable => {
43
- let {idsToStore, dict, effect, invalidationsCount} = inMemTable
41
+ ->IndexerState.effectState
42
+ ->EffectState.forEach(inMemTable => {
43
+ let {idsToStore, dict, effect, invalidationsCount, scope, table} = inMemTable
44
44
  switch idsToStore {
45
45
  | [] => ()
46
46
  | ids =>
@@ -51,17 +51,33 @@ let snapshotEffects = (state: IndexerState.t, ~cache): array<Persistence.updated
51
51
  }
52
52
  )
53
53
  let effectName = effect.name
54
- let effectCacheRecord = switch cache->Utils.Dict.dangerouslyGetNonOption(effectName) {
54
+ let tableName = table.tableName
55
+ let effectCacheRecord = switch cache->Utils.Dict.dangerouslyGetNonOption(tableName) {
55
56
  | Some(c) => c
56
57
  | None =>
57
- let c: Persistence.effectCacheRecord = {effectName, count: 0}
58
- cache->Dict.set(effectName, c)
58
+ let c: Persistence.effectCacheRecord = {effectName, scope, tableName, count: 0}
59
+ cache->Dict.set(tableName, c)
59
60
  c
60
61
  }
61
62
  let shouldInitialize = effectCacheRecord.count === 0
62
63
  effectCacheRecord.count = effectCacheRecord.count + items->Array.length - invalidationsCount
63
- Prometheus.EffectCacheCount.set(~count=effectCacheRecord.count, ~effectName)
64
- acc->Array.push(({effect, items, shouldInitialize}: Persistence.updatedEffectCache))->ignore
64
+ Prometheus.EffectCacheCount.set(
65
+ ~count=effectCacheRecord.count,
66
+ ~effectName,
67
+ ~scope=scope->Internal.EffectCache.scopeToString,
68
+ )
69
+ acc
70
+ ->Array.push(
71
+ (
72
+ {
73
+ table,
74
+ itemSchema: effect.storageMeta.itemSchema,
75
+ items,
76
+ shouldInitialize,
77
+ }: Persistence.updatedEffectCache
78
+ ),
79
+ )
80
+ ->ignore
65
81
  }
66
82
  inMemTable.idsToStore = []
67
83
  inMemTable.invalidationsCount = 0
@@ -192,8 +208,8 @@ let dropCommitted = (state: IndexerState.t, ~keepLoadedFromDb) => {
192
208
  ->InMemoryTable.Entity.dropCommittedChanges(~committedCheckpointId, ~keepLoadedFromDb)
193
209
  )
194
210
  state
195
- ->IndexerState.effects
196
- ->Utils.Dict.forEach(inMemTable =>
211
+ ->IndexerState.effectState
212
+ ->EffectState.forEach(inMemTable =>
197
213
  inMemTable->InMemoryStore.dropCommittedEffects(~committedCheckpointId, ~keepLoadedFromDb)
198
214
  )
199
215
  }
@@ -2,8 +2,10 @@
2
2
 
3
3
  import * as Env from "./Env.res.mjs";
4
4
  import * as Utils from "./Utils.res.mjs";
5
+ import * as Internal from "./Internal.res.mjs";
5
6
  import * as Throttler from "./Throttler.res.mjs";
6
7
  import * as Prometheus from "./Prometheus.res.mjs";
8
+ import * as EffectState from "./EffectState.res.mjs";
7
9
  import * as IndexerState from "./IndexerState.res.mjs";
8
10
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
9
11
  import * as InMemoryStore from "./InMemoryStore.res.mjs";
@@ -20,7 +22,7 @@ function getChangesCount(state) {
20
22
  IndexerState.allEntities(state).forEach(entityConfig => {
21
23
  total.contents = total.contents + InMemoryStore.getInMemTable(state, entityConfig).changesCount;
22
24
  });
23
- Utils.Dict.forEach(IndexerState.effects(state), inMemTable => {
25
+ EffectState.forEach(IndexerState.effectState(state), inMemTable => {
24
26
  total.contents = total.contents + inMemTable.changesCount;
25
27
  });
26
28
  IndexerState.processedBatches(state).forEach(batch => {
@@ -35,10 +37,12 @@ function waitForCommit(state) {
35
37
 
36
38
  function snapshotEffects(state, cache) {
37
39
  let acc = [];
38
- Utils.Dict.forEach(IndexerState.effects(state), inMemTable => {
40
+ EffectState.forEach(IndexerState.effectState(state), inMemTable => {
39
41
  let idsToStore = inMemTable.idsToStore;
40
42
  let invalidationsCount = inMemTable.invalidationsCount;
41
43
  let dict = inMemTable.dict;
44
+ let table = inMemTable.table;
45
+ let scope = inMemTable.scope;
42
46
  let effect = inMemTable.effect;
43
47
  if (idsToStore.length !== 0) {
44
48
  let items = Stdlib_Array.filterMap(idsToStore, id => {
@@ -51,23 +55,27 @@ function snapshotEffects(state, cache) {
51
55
  }
52
56
  });
53
57
  let effectName = effect.name;
54
- let c = cache[effectName];
58
+ let tableName = table.tableName;
59
+ let c = cache[tableName];
55
60
  let effectCacheRecord;
56
61
  if (c !== undefined) {
57
62
  effectCacheRecord = c;
58
63
  } else {
59
64
  let c$1 = {
60
65
  effectName: effectName,
66
+ scope: scope,
67
+ tableName: tableName,
61
68
  count: 0
62
69
  };
63
- cache[effectName] = c$1;
70
+ cache[tableName] = c$1;
64
71
  effectCacheRecord = c$1;
65
72
  }
66
73
  let shouldInitialize = effectCacheRecord.count === 0;
67
74
  effectCacheRecord.count = (effectCacheRecord.count + items.length | 0) - invalidationsCount | 0;
68
- Prometheus.EffectCacheCount.set(effectCacheRecord.count, effectName);
75
+ Prometheus.EffectCacheCount.set(effectCacheRecord.count, effectName, Internal.EffectCache.scopeToString(scope));
69
76
  acc.push({
70
- effect: effect,
77
+ table: table,
78
+ itemSchema: effect.storageMeta.itemSchema,
71
79
  items: items,
72
80
  shouldInitialize: shouldInitialize
73
81
  });
@@ -164,7 +172,7 @@ function commitBatch(state, batch) {
164
172
  function dropCommitted(state, keepLoadedFromDb) {
165
173
  let committedCheckpointId = IndexerState.committedCheckpointId(state);
166
174
  IndexerState.allEntities(state).forEach(entityConfig => InMemoryTable.Entity.dropCommittedChanges(InMemoryStore.getInMemTable(state, entityConfig), committedCheckpointId, keepLoadedFromDb));
167
- Utils.Dict.forEach(IndexerState.effects(state), inMemTable => InMemoryStore.dropCommittedEffects(inMemTable, committedCheckpointId, keepLoadedFromDb));
175
+ EffectState.forEach(IndexerState.effectState(state), inMemTable => InMemoryStore.dropCommittedEffects(inMemTable, committedCheckpointId, keepLoadedFromDb));
168
176
  }
169
177
 
170
178
  async function awaitCapacity(state) {
@@ -173,5 +173,10 @@ module Fs = {
173
173
 
174
174
  @module("fs") @scope("promises")
175
175
  external readdir: Path.t => promise<array<string>> = "readdir"
176
+
177
+ type stats
178
+ @module("fs") @scope("promises")
179
+ external stat: Path.t => promise<stats> = "stat"
180
+ @send external statsIsDirectory: stats => bool = "isDirectory"
176
181
  }
177
182
  }